From 7e87e8f01ca57dd216c02d6f4f8dfc834d5c9c72 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 15:54:05 +0000 Subject: [PATCH 1/2] Add .claude/skills continuity library for maintainers Introduce a 15-skill library under .claude/skills/ to let junior/mid-level engineers and smaller AI models maintain, debug, extend, and advance Lighthouse without loss of standard. Core: change-control, debugging-playbook, failure-archaeology, architecture-contract, graphql-laravel-reference, config-and-flags, build-and-env, testing-and-qa, diagnostics-and-tooling (with runnable scripts/), docs-and-writing, issue-triage-and-reproduction. Advanced: v7-campaign (decision-gated major-release runbook), proof-and-analysis-toolkit, research-frontier, research-methodology. Every command, flag, path, commit, and claim verified against the repo at v6.68.0. Volatile facts are date-stamped with re-verification commands. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Hvx6ybvjs3XU8TeHchLswP --- .../skills/graphql-laravel-reference/SKILL.md | 117 +++++++++++++ .../lighthouse-architecture-contract/SKILL.md | 96 +++++++++++ .../skills/lighthouse-build-and-env/SKILL.md | 118 +++++++++++++ .../skills/lighthouse-change-control/SKILL.md | 159 ++++++++++++++++++ .../lighthouse-config-and-flags/SKILL.md | 128 ++++++++++++++ .../lighthouse-debugging-playbook/SKILL.md | 93 ++++++++++ .../SKILL.md | 117 +++++++++++++ .../scripts/bench-report.sh | 34 ++++ .../scripts/config-keys.php | 46 +++++ .../scripts/schema-diff.sh | 48 ++++++ .../lighthouse-docs-and-writing/SKILL.md | 118 +++++++++++++ .../lighthouse-failure-archaeology/SKILL.md | 108 ++++++++++++ .../SKILL.md | 153 +++++++++++++++++ .../SKILL.md | 97 +++++++++++ .../lighthouse-research-frontier/SKILL.md | 83 +++++++++ .../lighthouse-research-methodology/SKILL.md | 93 ++++++++++ .../skills/lighthouse-testing-and-qa/SKILL.md | 127 ++++++++++++++ .../skills/lighthouse-v7-campaign/SKILL.md | 131 +++++++++++++++ 18 files changed, 1866 insertions(+) create mode 100644 .claude/skills/graphql-laravel-reference/SKILL.md create mode 100644 .claude/skills/lighthouse-architecture-contract/SKILL.md create mode 100644 .claude/skills/lighthouse-build-and-env/SKILL.md create mode 100644 .claude/skills/lighthouse-change-control/SKILL.md create mode 100644 .claude/skills/lighthouse-config-and-flags/SKILL.md create mode 100644 .claude/skills/lighthouse-debugging-playbook/SKILL.md create mode 100644 .claude/skills/lighthouse-diagnostics-and-tooling/SKILL.md create mode 100755 .claude/skills/lighthouse-diagnostics-and-tooling/scripts/bench-report.sh create mode 100644 .claude/skills/lighthouse-diagnostics-and-tooling/scripts/config-keys.php create mode 100755 .claude/skills/lighthouse-diagnostics-and-tooling/scripts/schema-diff.sh create mode 100644 .claude/skills/lighthouse-docs-and-writing/SKILL.md create mode 100644 .claude/skills/lighthouse-failure-archaeology/SKILL.md create mode 100644 .claude/skills/lighthouse-issue-triage-and-reproduction/SKILL.md create mode 100644 .claude/skills/lighthouse-proof-and-analysis-toolkit/SKILL.md create mode 100644 .claude/skills/lighthouse-research-frontier/SKILL.md create mode 100644 .claude/skills/lighthouse-research-methodology/SKILL.md create mode 100644 .claude/skills/lighthouse-testing-and-qa/SKILL.md create mode 100644 .claude/skills/lighthouse-v7-campaign/SKILL.md diff --git a/.claude/skills/graphql-laravel-reference/SKILL.md b/.claude/skills/graphql-laravel-reference/SKILL.md new file mode 100644 index 000000000..d6e80b0ca --- /dev/null +++ b/.claude/skills/graphql-laravel-reference/SKILL.md @@ -0,0 +1,117 @@ +--- +name: graphql-laravel-reference +description: >- + Domain-theory pack for engineers who know PHP/Laravel but not GraphQL internals, + or vice versa. Load when a task needs the meaning of GraphQL/graphql-php/Eloquent + concepts AS USED IN THIS REPO — SDL and directive definitions, executable schema + and introspection, the type-loader vs types callable (laziness), DebugFlag, + Eloquent-relation-to-directive mapping, batch loading, pagination types + (PAGINATOR/SIMPLE/CONNECTION), Relay/Federation/APQ/tracing/@defer, or the jargon + glossary. Anchors every concept to a file path. For design rationale use + lighthouse-architecture-contract; for config values use lighthouse-config-and-flags. +--- + +# GraphQL + Laravel reference (as applied in Lighthouse) + +The knowledge a mid-level engineer or smaller model is missing to work here. Not a textbook — every concept points at a file in this repo. + +Ground truth date: 2026-07-07, v6.68.0 (`master` @ a29ff8e). + +## 1. GraphQL for this repo + +**SDL (Schema Definition Language)** — the text format describing types/fields. Lighthouse is *schema-first*: you write SDL, it builds the runtime. Example: `src/default-schema.graphql`: +```graphql +type Query { + user(id: ID @eq): User @find + users(name: String @where(operator: "like")): [User!]! @paginate(defaultCount: 10) +} +``` + +**Directive** — a `@foo` annotation attached to schema elements that drives server behavior. Every directive is a PHP class whose `definition(): string` returns its own SDL. Real example (`src/Schema/Directives/FieldDirective.php`): +```graphql +directive @field(resolver: String!) on FIELD_DEFINITION +``` +`@field(resolver: "Class@method")` assigns a resolver; method defaults to `__invoke`. The SDL `on FIELD_DEFINITION` part is a **type-system directive location** — where the directive may legally appear. (Contrast **executable directives** like `@include`/`@skip` that appear in client queries.) + +**Executable schema** — the in-memory `GraphQL\Type\Schema` object produced from the SDL, used to validate and execute queries. Built by `src/Schema/SchemaBuilder.php`. + +**Introspection** — the built-in query API (`__schema`, `__type`) that lets clients discover the schema. It needs the full type list, which is why `SchemaBuilder` registers a `types` callable (`$config->setTypes(...)`). That callable's eager resolution is the crux of #2771. + +**GraphQL over HTTP** — Lighthouse serves one endpoint (default `/graphql`), accepting POST (and GET), following the informal spec at graphql.org. Errors come back in the spec shape `{ "errors": [{ "message", "extensions", "locations", "path" }], "data": … }`. + +**Operations / variables / fragments** — a request has an operation (query/mutation/subscription), optional named variables, and reusable fragments. In tests these appear as `/** @lang GraphQL */` nowdocs (see `lighthouse-testing-and-qa`). + +## 2. webonyx/graphql-php essentials used here + +- **`Schema` + `SchemaConfig`** — the config carries `typeLoader` (lazy, per-name: `fn ($name) => $typeRegistry->search($name)`) and `types` (eager list for introspection: `fn () => $typeRegistry->possibleTypes()`). The split of these two is Lighthouse's laziness mechanism; #2771 is the failure of that split under graphql-php ≥ 15.31. +- **`DocumentNode` / AST** — the parsed SDL. Lighthouse manipulates the AST (`src/Schema/AST/ASTBuilder.php`) — expanding `@paginate`, `@orderBy`, etc. — *before* building the executable schema, so transformations happen once and are cacheable. +- **`DebugFlag`** — a bitmask (`INCLUDE_DEBUG_MESSAGE`, `INCLUDE_TRACE`, `RETHROW_*`) controlling error verbosity; mapped to integers 0–15 in `src/lighthouse.php` `debug`. Only applied when Laravel `app.debug` is true. +- **Validation rules** — `QueryComplexity`, `QueryDepth`, `DisableIntrospection` (config `security`, all DISABLED by default). +- **Default field resolver** — graphql-php's fallback resolver reads array keys / object properties / `offsetExists`; Lighthouse overrides resolution via directives and `FieldValue`. +- **`ClientAware` errors** — graphql-php interface marking errors safe to show clients. Lighthouse's client-safe exceptions implement it: `src/Exceptions/DefinitionException.php`, `DirectiveException.php`, `RateLimitException.php`, `ClientSafeModelNotFoundException.php`, `SchemaSyntaxErrorException.php`. Non-`ClientAware` exceptions are masked unless a RETHROW debug flag is set. + +## 3. Eloquent / Laravel mapped to Lighthouse + +**Relations → directives** (all in `src/Schema/Directives/`): `@hasMany`, `@hasOne`, `@hasManyThrough`, `@hasOneThrough`, `@belongsTo`, `@belongsToMany`, `@morphOne`, `@morphMany`, `@morphTo`, `@morphToMany`. Each maps a GraphQL field to the corresponding Eloquent relation. + +**Batch loading** — resolving a relation for a list of parents naively causes **N+1** (one query per parent). Lighthouse's `RelationBatchLoader` (`src/Execution/BatchLoader/`) collects all parents and issues one query per relation, then distributes results. Controlled by `batchload_relations` (default on). Model loaders in `src/Execution/ModelsLoader/` (Simple/Paginated/Count/Aggregate) implement the strategies. + +**Pagination types** (`src/Pagination/PaginationType.php`): `PAGINATOR` (page-number based, with `paginatorInfo`), `SIMPLE` (has-more without total count), `CONNECTION` (Relay cursor-based). `@paginate(type: …)` selects one. Changing the generated pagination types in the schema is a classic breaking change (see #2104 in `lighthouse-failure-archaeology`). + +**Mass assignment & `force_fill`** — Lighthouse uses `forceFill()` in mutations because GraphQL's schema already constrains inputs, making Laravel's `$fillable`/`$guarded` redundant (`force_fill` config, default on). + +**Policies / gates → `@can*` family** (`src/Auth/`): `@canModel`, `@canFind`, `@canQuery`, `@canResolved`, `@canRoot` (the old single `@can` is `@deprecated` for v7). These call Laravel Gate/Policy checks at the field level. + +**Validation → `@rules`/Validators** (`src/Validation/`): map Laravel validation rules onto arguments/inputs; custom Validator classes live under the `validators` namespace. + +**Queues → subscription broadcasts** — subscription broadcasts can be queued (`subscriptions.queue_broadcasts`). + +**Laravel vs Lumen** — Lighthouse supports both; hence no Facades in `src/` and DI everywhere (see `lighthouse-architecture-contract`). + +## 4. Ecosystem protocols as implemented + +- **Relay** — Node interface + global object IDs (`src/GlobalId/`), and cursor **connections** (`src/Pagination/ConnectionField.php`, `PaginationType::CONNECTION`). +- **Apollo Federation** — Lighthouse can act as a subgraph: `src/Federation/` provides entity resolution (`EntityResolverProvider`, `BatchedEntityResolver`), the federated schema printer (`FederationPrinter`), and `_entities`/`_service` support. Open asks: #2582, #2482. +- **Automatic Persisted Queries (APQ)** — clients send a query hash; the server caches the full query. Handled in `src/GraphQL.php` (`persisted_queries` config, default on; Apollo-compatible). +- **Tracing** — `src/Tracing/`: `ApolloTracing` (JSON, current default) and `FederatedTracing` (protobuf `reports.proto`, v7 default). +- **Incremental delivery (`@defer`)** — `src/Defer/` (`DeferrableDirective`), experimental per the `defer` config comment; streams deferred fields after the initial response. + +## 5. Glossary + +| Term | One-line meaning | +|---|---| +| SDL | GraphQL's schema text format. | +| AST | Parsed tree of the SDL/query that Lighthouse manipulates. | +| Directive | `@foo` annotation → `FooDirective` class driving behavior. | +| Resolver | Function producing a field's value. | +| Directive locator | Maps directive names ↔ classes by namespace (`DirectiveLocator`). | +| Type loader | Lazy per-name type factory (`SchemaConfig::setTypeLoader`). | +| Batch loader | Dedupes relation queries across a result set to kill N+1. | +| N+1 | One query per parent row instead of one batched query. | +| Introspection | Built-in schema-discovery query API. | +| Connection / cursor | Relay pagination shape / opaque position pointer. | +| Paginator | Page-number pagination with total count. | +| APQ | Automatic Persisted Queries (send hash, cache full query). | +| Federation / subgraph | Composing multiple GraphQL services; Lighthouse is a subgraph. | +| Field middleware | Directives wrapping every field's resolution, ordered. | +| ClientAware | graphql-php marker for errors safe to expose to clients. | +| DebugFlag | Bitmask controlling error verbosity. | + +## When NOT to use this skill + +- WHY the system is built this way / invariants → `lighthouse-architecture-contract`. +- Config keys and defaults → `lighthouse-config-and-flags`. +- Diagnosing a live failure → `lighthouse-debugging-playbook`. +- Writing tests that exercise these features → `lighthouse-testing-and-qa`. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e). Re-verify with: + +```bash +grep -n "PAGINATOR\|SIMPLE\|CONNECTION" src/Pagination/PaginationType.php +ls src/Schema/Directives/ | grep -iE "hasmany|belongsto|morph" +ls src/Federation/ src/Defer/ src/GlobalId/ src/Tracing/ +grep -rln "ClientAware" src/Exceptions/ +grep -n "setTypeLoader\|setTypes" src/Schema/SchemaBuilder.php +``` diff --git a/.claude/skills/lighthouse-architecture-contract/SKILL.md b/.claude/skills/lighthouse-architecture-contract/SKILL.md new file mode 100644 index 000000000..e64504a72 --- /dev/null +++ b/.claude/skills/lighthouse-architecture-contract/SKILL.md @@ -0,0 +1,96 @@ +--- +name: lighthouse-architecture-contract +description: >- + Understand Lighthouse's load-bearing design decisions, the invariants that must + hold, and the known-weak points before changing internals. Load when touching the + schema build pipeline (ASTBuilder, SchemaBuilder, TypeRegistry, DirectiveLocator), + the execution path (BatchLoader, ModelsLoader, Arguments), directive interfaces, + service-provider registration, or anything whose correctness depends on laziness, + serializability, transaction semantics, or Lumen/Laravel dual support. Explains + WHY the system is shaped this way and what breaks if an invariant is violated. +--- + +# Lighthouse architecture contract + +Lighthouse is a schema-first GraphQL framework for Laravel: you write GraphQL SDL, annotate it with server-side **directives**, and Lighthouse turns that into an executable schema backed by Eloquent. +This skill is the map of the load-bearing structure — the decisions you must not casually undo and the invariants you must not break. + +Ground truth date: 2026-07-07, v6.68.0 (`master` @ a29ff8e). Every path and class below was read from source. + +## System map: request lifecycle and owning classes + +Reference doc: `docs/master/concepts/request-lifecycle.md`. The runtime path: + +| Stage | Owner | Notes | +|---|---|---| +| Route registration | `src/Http/routes.php` → `src/Http/GraphQLController.php` | Single endpoint (`lighthouse.route`, default `/graphql`); works under Laravel Router or Lumen Router. | +| HTTP middleware | `src/Http/Middleware/` (`AcceptJson`, `AttemptAuthentication`, `EnsureXHR`, `LogGraphQLQueries`) | Configured in `lighthouse.route.middleware`. Runs before GraphQL execution. | +| Request parsing | `laragraph/utils` + `src/Http/` | Parses HTTP into GraphQL operation(s); follows the informal GraphQL-over-HTTP spec. | +| Schema construction | `src/Schema/Source/SchemaStitcher` → `src/Schema/AST/ASTBuilder.php` → `src/Schema/SchemaBuilder.php` → `src/Schema/TypeRegistry.php` | Cached (see schema cache). Directives manipulate the AST here. | +| Directive resolution | `src/Schema/DirectiveLocator.php` | Maps `@foo` ↔ `FooDirective` by namespace priority. | +| Query validation | `webonyx/graphql-php` `DocumentValidator` via `src/GraphQL.php` | Plus `security` rules (complexity/depth/introspection). Cacheable (`validation_cache`). | +| Field execution | `src/Execution/` + `src/Schema/Factories/FieldFactory.php` | Field middleware pipeline wraps resolvers; batch loading dedupes relation queries. | +| Error handling | `src/Execution/` error handlers (`error_handlers` config) + `ErrorPool` | Errors collected, subtree aborted, rest continues. | +| Response assembly | `src/GraphQL.php` (`@api` entrypoint) | Debug flags applied per `lighthouse.debug`. | + +`src/GraphQL.php` is the `@api`-marked entrypoint (`executeQueryString`, `executeParsedQuery`, `executeOperationOrOperations`). + +## Load-bearing decisions (and WHY) + +1. **Schema-first SDL + directive-driven AST manipulation** (not code-first). Users declare intent in `.graphql`; directives rewrite the `DocumentAST` before the executable schema is built (`ASTBuilder`). Why: keeps the schema the single source of truth and makes behavior declarative. Consequence: most features are directives, and schema transformations happen once at build time, not per request. + +2. **Directive naming convention + namespace discovery.** `FooDirective` ⇄ `@foo`. `DirectiveLocator::className()` does `Str::studly($name) . 'Directive'`; `directiveName()` is the inverse. Namespaces are tried in priority order (`config('lighthouse.namespaces.directives')` first, then Lighthouse's own — see `DirectiveLocator::namespaces()`), so user directives can override built-ins. Why: zero-registration extensibility. + +3. **One service provider per optional feature.** `composer.json` → `extra.laravel.providers` lists ~11 providers (Auth, Cache, GlobalId, OrderBy, Pagination, SoftDeletes, Testing, Validation, Async, Bind, plus the core `LighthouseServiceProvider`). CacheControl, Federation, Pennant, Scout, Subscriptions register conditionally. Why: features stay decoupled and optional; a user who does not need Scout never loads it. + +4. **Laziness is a design goal.** `SchemaBuilder::build()` sets `$config->setTypeLoader(fn ($name) => $this->typeRegistry->search($name))` so types are built on demand, not all upfront. This matters because the schema is rebuilt per request under PHP-FPM when the schema cache is cold or disabled. **Known breach:** `$config->setTypes(fn () => $this->typeRegistry->possibleTypes())` (needed for introspection) is a lazy callable, but graphql-php ≥ 15.31 resolves it on the first built-in scalar lookup, defeating laziness — issue **#2771, OPEN** (see Known-weak points). + +5. **Field middleware pipeline order is config-defined and significant.** `lighthouse.field_middleware` lists directives that wrap every field, executed in array order: `TrimDirective`, `ConvertEmptyStringsToNullDirective`, `SanitizeDirective`, `ValidateDirective`, `TransformArgsDirective`, `SpreadDirective`, `RenameArgsDirective`, `DropArgsDirective`. Why order matters: sanitization must precede validation; `@spread` must flatten input before renames/drops act on it. Do not reorder without understanding each stage. + +6. **Transactional mutations commit after the root field.** `lighthouse.transactional_mutations` (default `true`) wraps built-in model-mutating directives in a DB transaction that is committed **after the root field resolves** — so errors in **nested** fields do **not** roll back the root mutation (documented in the `src/lighthouse.php` comment). This is deliberate; changing it silently changes data-integrity semantics for every user. + +7. **`force_fill` over `fill`.** `lighthouse.force_fill` (default `true`) bypasses Laravel mass-assignment protection in mutation directives, because GraphQL already constrains allowed inputs by schema. Why: mass-assignment guards are redundant and would reject valid GraphQL input. + +8. **Lumen + Laravel dual support → no Facades, DI everywhere.** CONTRIBUTING.md forbids Facades (Lumen may not enable them) and prefers direct Illuminate classes over helpers. Sanctioned exception: the `response()` helper (DI of `ResponseFactory` does not work in Lumen). `src/Http/routes.php` resolves the router defensively for both frameworks. + +9. **Extensibility contract:** `protected` over `private`, never `final` in `src/`, `@api` marks the stable surface (33 annotations). See `lighthouse-change-control` for the enforcement rules. + +## Invariants (what must hold, and how to check) + +| Invariant | Why it must hold | Breaks if violated | Check | +|---|---|---|---| +| Schema consumers never break within a major | Users `composer update` blindly within `^6` | Every downstream API breaks at once | `lighthouse:print-schema` diff = empty (`lighthouse-change-control`) | +| Types resolve lazily per request | Per-request schema rebuild must be cheap | Latency + memory blow up (see #2771) | `grep -n "setTypeLoader" src/Schema/SchemaBuilder.php` | +| Every directive has an SDL definition | Schema validation + IDE helper need it | Schema build throws / directive ignored | `src/Support/Contracts/Directive.php` requires `public static function definition(): string` | +| `DocumentAST` stays serializable | Schema cache serializes it to a PHP file | Schema cache write/read fails | `src/Schema/AST/DocumentAST.php`, `ASTCache` | +| Nested-field errors don't roll back the root mutation | Documented transaction semantics | Silent data-integrity change | `src/Execution/TransactionalMutations.php`; `src/lighthouse.php` `transactional_mutations` comment | +| No Facades in `src/` | Lumen compatibility | Lumen apps fatal | `grep -rn "Facade\|Illuminate\\\\Support\\\\Facades" src` should be empty | + +## Known-weak points (stated plainly — all OPEN as of 2026-07-07) + +- **#2771** — Lazy schema becomes eager on every request with `webonyx/graphql-php` ≥ 15.31. First built-in scalar lookup triggers scalar-override discovery, which resolves the `setTypes` callable → `TypeRegistry::possibleTypes()` builds every type. Measured 1.5–3.4× slower, ~6× PHP work. Fix depends on upstream `SchemaConfig::setScalarOverrides()`. This is the single biggest architectural liability right now. +- **#2758** — `batchload_relations` bypasses `@canResolved(action: RETURN_VALUE)`; authorization result ignored for batched relations. +- **#2679** — Laravel 12's `Model::automaticallyEagerLoadRelationships()` is incompatible with Lighthouse's `RelationBatchLoader` (`Collection::relationLoaded does not exist`). +- **#2744** — Nested mutations run unscoped queries (`$relation->getRelated()::destroy($ids)`, `$model->newQuery()->findOrFail($id)`), letting a mutation touch records outside the parent relationship. Fix is breaking → deferred to v7. See `lighthouse-failure-archaeology`. +- **MySQL 5.7 test pin** — `docker-compose.yml` pins `mysql:5.7` with `# TODO switch to MySQL 8` (issue #1784). +- **PHPStan level 8, not max** — `phpstan.neon` `# TODO level up to max`. + +## When NOT to use this skill + +- Rules for gating/versioning a change → `lighthouse-change-control`. +- Every config key and its default → `lighthouse-config-and-flags`. +- GraphQL/Eloquent domain concepts → `graphql-laravel-reference`. +- The history of how a decision was reached → `lighthouse-failure-archaeology`. +- Diagnosing a live failure → `lighthouse-debugging-playbook`. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e). Re-verify with: + +```bash +grep -n "setTypeLoader\|setTypes\|possibleTypes" src/Schema/SchemaBuilder.php +grep -n "className\|directiveName\|studly" src/Schema/DirectiveLocator.php +grep -rn "final " src/ | grep -v "finally" # should be empty in src/ +grep -n "transactional_mutations\|force_fill" src/lighthouse.php +sed -n '/extra/,/scripts/p' composer.json | grep -i provider # service provider list +``` diff --git a/.claude/skills/lighthouse-build-and-env/SKILL.md b/.claude/skills/lighthouse-build-and-env/SKILL.md new file mode 100644 index 000000000..f91f0e460 --- /dev/null +++ b/.claude/skills/lighthouse-build-and-env/SKILL.md @@ -0,0 +1,118 @@ +--- +name: lighthouse-build-and-env +description: >- + Recreate and operate the Lighthouse development environment. Load when setting up + the project, running make targets, choosing Docker+Make vs native tooling, + understanding the docker-compose services (php/mysql/redis/node), configuring the + test database, regenerating protobuf or agent config, or diagnosing environment + traps (Xdebug slowness, auto-format commits, gitignored phpunit.xml, packagist + egress blocks, CI removing dev deps). For what the test suites assert and how to + add tests use lighthouse-testing-and-qa; for measurement tools use + lighthouse-diagnostics-and-tooling. +--- + +# Lighthouse build and environment + +Two supported paths: **Docker + Make** (reproducible, recommended) and **native tools**. This skill lets you stand up either from scratch. + +Ground truth date: 2026-07-07, v6.68.0 (`master` @ a29ff8e). Verified by reading `Makefile`, `docker-compose.yml`, `php.dockerfile`, `node.dockerfile`, `composer.json`, `phpunit.xml.dist`, `.github/workflows/validate.yml`, and `.ai/`. Commands here were **config-verified, not executed** in the authoring environment (which has no `vendor/`). + +## Docker + Make happy path + +```bash +make setup # build containers + install deps + docs deps + generate agent config +make it # the pre-commit gate: vendor + fix + stan + test +``` + +### Every Make target (from `Makefile`) + +| Target | Does | +|---|---| +| `make help` | List targets with descriptions. | +| `make setup` | `build` + `vendor` + `docs/node_modules` + `ai-sync`. | +| `make build` | Build Docker images, passing `USER_ID`/`GROUP_ID` (file-ownership; short `id` opts for macOS, PR #2504). | +| `make it` | `vendor fix stan test` — run before commits. | +| `make fix` | `rector` + `php-cs-fixer` + `prettier`. | +| `make rector` | Refactor via Rector. | +| `make php-cs-fixer` | Format PHP. | +| `make prettier` | Format docs markdown/js (via `node-docs`). | +| `make stan` | PHPStan (level 8). | +| `make test` | PHPUnit. | +| `make bench` | PHPBench aggregate report. | +| `make vendor` | `composer update` + `composer validate --strict` + `composer normalize`. | +| `make php` | Interactive shell in php container. (Denied in `.ai/settings.json` for agents.) | +| `make node` | Interactive shell in node container. (Denied for agents.) | +| `make release` | `rm -rf docs/6 && cp -r docs/master docs/6` — **hardcodes `docs/6`**; a new major must edit this. | +| `make docs` | Dev server for docs (port 8081). | +| `make docs/node_modules` | Install docs yarn deps. | +| `make ai-sync` | Regenerate agent config from `.ai/` via `lnai@0.6.7`. | +| `make proto` | Regenerate protobuf PHP classes with `buf`. | +| `make proto/update-reports` | Refresh `reports.proto` from Apollo. | + +### Single test + +```bash +docker compose run --rm php vendor/bin/phpunit --filter=TestClassName +docker compose run --rm php vendor/bin/phpunit --filter=testMethodName +docker compose run --rm php vendor/bin/phpunit tests/Unit/Path/To/TestFile.php +``` +(From `.ai/AGENTS.md`.) + +## Services (`docker-compose.yml`) + +| Service | Image / build | Notes | +|---|---|---| +| `php` | `php.dockerfile` (`php:8.3-cli` + zip, mysqli, pdo_mysql, intl, bcmath, xdebug, redis; `memory_limit=-1`) | Build args `USER_ID`/`GROUP_ID` for file ownership. | +| `mysql` | `mysql:5.7`, `tmpfs:/var/lib/mysql`, `platform: linux/amd64` | Pinned 5.7 (`# TODO switch to MySQL 8`, issue #1784). `platform` pin is for Apple Silicon. Empty root password. | +| `redis` | `redis:6` | Subscription storage / cache tests. | +| `node-docs` | `node.dockerfile` (`node:22-slim`) | Docs dev server, port 8081. | +| `node-tools` | `node.dockerfile` | Used by `make ai-sync` (lnai). | + +## Native path + +Requirements: PHP `^8` with extensions `mbstring, mysqli, pdo_mysql, redis` (from `validate.yml` `REQUIRED_PHP_EXTENSIONS`; `intl`/`bcmath`/`zip` also used per the Dockerfile), Composer 2, MySQL (any Laravel-supported version), Redis 6. + +```bash +composer install +cp phpunit.xml.dist phpunit.xml # then edit the DB/Redis params to reach your instances +vendor/bin/phpunit # run tests +vendor/bin/phpstan --verbose # static analysis +vendor/bin/php-cs-fixer fix # format +``` +`composer install` runs `post-autoload-dump` → `testbench package:discover` (`composer.json` scripts). +`phpunit.xml` is **gitignored** (per-developer) — you must copy it. + +## Known traps (each verified) + +- **Xdebug slows tests ~10×.** The php image installs Xdebug. CONTRIBUTING.md: "Enabling Xdebug slows down tests by an order of magnitude. Stop listening for Debug Connection to speed it back up." Set `XDEBUG_REMOTE_HOST` to your host IP as seen from the container. +- **The format workflow auto-commits to your branch.** `.github/workflows/format.yml` runs `composer normalize` + Prettier on every push and pushes the result back (`git-auto-commit-action`, `creyD/prettier_action`). After pushing, `git pull` before continuing or you'll hit a non-fast-forward. +- **`phpunit.xml` is gitignored.** Copy from `phpunit.xml.dist`; your edits stay local. +- **`vendor/` is absent until `composer install`.** You cannot run phpunit/phpstan/phpbench without it. A cloud/CI sandbox with restricted egress may block packagist entirely (symptom: `composer` fails with a proxy `CONNECT … 403`). If so, dependency-requiring commands can't run there — verify by reading config instead. +- **CI removes some dev deps.** `validate.yml` runs `composer remove --dev --no-update phpbench/phpbench rector/rector` (and `larastan`, `phpstan-mockery` in the test job) because they conflict on the lowest/oldest matrix cells, and removes `laravel/pennant` on Laravel 9 (incompatible). So CI runs a slightly different dependency set than your local highest-deps install — see `lighthouse-testing-and-qa` for why "green locally" ≠ done. +- **MySQL 5.7 pin.** Tests target MySQL 5.7; behavior may differ on MySQL 8 until #1784 is resolved. +- **Proto regeneration needs `buf`.** `make proto` runs `bufbuild/buf` via Docker and `sudo chown`s the output; don't hand-edit `src/Tracing/FederatedTracing/Proto/`. + +## Agent configuration (how `.claude` relates to `.ai`) + +- `.ai/AGENTS.md` is the **source** of agent guidance; `.ai/config.json` and `.ai/settings.json` configure which tools get generated files and their permissions. +- `make ai-sync` runs `lnai` to generate tool-specific files. The generated `.claude/CLAUDE.md` and `.claude/settings.json` are **gitignored** (`.gitignore` → `# lnai-generated`). +- **`.claude/skills/` is version-controlled** — that is where this skill library lives. It is not generated by lnai and is not gitignored. + +## When NOT to use this skill + +- What the tests assert / how to add one → `lighthouse-testing-and-qa`. +- Diagnostic/measurement tooling (benchmarks, query counts, tracing) → `lighthouse-diagnostics-and-tooling`. +- Config option meanings → `lighthouse-config-and-flags`. +- Running/operating a Lighthouse app as a user would → not covered here; this is the *dev* environment for the library itself. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e); commands config-verified, not executed. Re-verify with: + +```bash +grep -E "^\.PHONY|##" Makefile | head -40 # target list + descriptions +grep -n "image:\|platform:\|tmpfs" docker-compose.yml +grep -n "REQUIRED_PHP_EXTENSIONS\|composer remove" .github/workflows/validate.yml +grep -n "lnai\|ai-sync" Makefile +sed -n '/lnai-generated/,/end lnai-generated/p' .gitignore +``` diff --git a/.claude/skills/lighthouse-change-control/SKILL.md b/.claude/skills/lighthouse-change-control/SKILL.md new file mode 100644 index 000000000..9b8cd5a8f --- /dev/null +++ b/.claude/skills/lighthouse-change-control/SKILL.md @@ -0,0 +1,159 @@ +--- +name: lighthouse-change-control +description: >- + Gate and classify any change to Lighthouse before committing or opening a PR. + Use when deciding whether a change is a fix/feature/deprecation/breaking change, + what target version it belongs in, whether it needs a CHANGELOG or UPGRADE.md entry, + or whether it breaks the @api contract or a schema consumer. Load before merging, + releasing, bumping a dependency constraint, changing a config default, or altering + schema output, response shape, or error messages. Encodes the project's + non-negotiables (SemVer, @api stability, no breaking changes for schema consumers) + with their rationale and the incidents behind them. +--- + +# Lighthouse change control + +This is the gate every change passes through. +Read it before you commit, before you open a PR, and before you cut a release. +Lighthouse is a widely-deployed library: a bad release breaks thousands of production GraphQL APIs at once, and there is no staged rollout. +The bar is correctness and backward compatibility, not speed. + +Ground truth date: 2026-07-07, verified against v6.68.0 (`master` @ a29ff8e). + +## First: classify the change + +| Class | CHANGELOG entry? | Tests required? | Docs (`/docs`) | UPGRADE.md | Target version | +|---|---|---|---|---|---| +| Docs-only | No (policy) | No | The change itself | No | any | +| Bug fix | Yes — `Fixed` | Yes — failing test first | If behavior was documented | No | current major, next patch | +| New feature | Yes — `Added` | Yes | Yes | No | current major, next minor | +| Deprecation | Yes — `Deprecated` | Keep tests green | Note the replacement | No (until removal) | current major, next minor | +| Breaking change | Yes — `Changed`/`Removed` | Yes | Yes | **Yes, required** | **next major only** | + +The "docs-only changes need no CHANGELOG entry" rule is explicit project policy — see commit `b095405` ("Clarify @search empty/null semantics and docs-only changelog policy") and CONTRIBUTING.md → Changelog. + +CHANGELOG entries go at the top of the `## Unreleased` section in `CHANGELOG.md`, in the correct category, and **end with the full PR URL**: `https://github.com/nuwave/lighthouse/pull/`. +Categories are fixed (Keep a Changelog): `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`. + +## The non-negotiables (with rationale and the incident behind each) + +### 1. Semantic Versioning; only the current major gets features and fixes + +README → Versioning: "Only the current major version receives new features and bugfixes." +Minor upgrades must require **no** changes to a user's PHP code or GraphQL schema, and cause **no** breaking behavioral change for API consumers. +Rationale: users pin `^6` and expect `composer update` within a major to be safe forever. + +### 2. The `@api` PHPDoc contract is the code-stability boundary + +Code elements marked with the `@api` PHPDoc tag are guaranteed stable until the next major (CONTRIBUTING.md → Internal). +Everything **not** marked `@api` is internal and may change in any release. +There are currently 33 `@api` annotations across `src` (`grep -rc "@api" src` to recount). +Load-bearing examples to respect: `src/Support/Contracts/GraphQLContext.php`, `src/Support/Contracts/ArgResolver.php`, `src/Support/Contracts/SaveAwareArgResolver.php`, `src/Execution/ErrorHandler.php`, `src/Schema/TypeRegistry.php`, `src/Schema/Directives/BaseDirective.php`. +Before changing any signature, run `grep -n "@api" ` — if the method or class carries it, the change is breaking and goes to the next major. + +### 3. No breaking changes for schema consumers within a major (the unwritten rule, now written) + +This is stronger than the `@api` code contract and is the maintainer's hardest line. +A client of a running Lighthouse GraphQL API must never observe breakage from a minor/patch upgrade. Frozen surfaces: + +- **Schema output** — the printed SDL (types, fields, nullability, directive locations, enum values). +- **Response shapes** — the JSON structure of query/mutation/subscription results, including pagination wrappers. +- **Error semantics** — error `message`, `extensions`, categories, and where errors surface. + +Incidents that set this rule: + +- **`88d8e18f` "Revert breaking schema change in generate pagination types (#2104)"** — a change altered the generated pagination types in the printed schema; it shipped, broke consumers' schemas, and was reverted with entries added to both CHANGELOG.md and UPGRADE.md. Touches `src/Pagination/PaginationManipulator.php`. Lesson: any diff to generated schema types is breaking even if the PHP API is untouched. +- **`150b5401` "Prevent regression to simple paginator type on fields using `@cache` (#2355)"** — `@cache` combined with pagination silently downgraded the paginator type in the schema; caught and fenced with a regression test. Lesson: feature interactions can break schema output; test the combinations. + +How to check before you ship: see "Is my change breaking?" below. + +### 4. Full support-matrix compatibility + +CI (`.github/workflows/validate.yml`) runs PHPStan and PHPUnit across **PHP 8.0–8.5 × Laravel ^9–^13 × lowest/highest dependencies** (with documented exclusions). +"Works on my PHP 8.4 / Laravel 12" is not evidence. +A change is not done until the whole matrix is green. +Do not drop a supported PHP/Laravel version in a minor — that is breaking (next major only). + +### 5. Extensibility guarantees + +From CONTRIBUTING.md → Code Guidelines: + +- Use `protected` over `private` everywhere in `src/` — subclasses must be able to override. +- Never use `final` in `src/`; always use `final` in `tests/`. +- Full namespace in PHPDoc (`@var \Full\Namespace\Class`), imports in code. +- No Laravel Facades (Lumen compatibility) — inject dependencies. The one sanctioned exception is the `response()` helper. + +Violating these does not break users today but removes an escape hatch they rely on, so reviewers treat it as blocking. + +## Review and merge gates + +Run before every commit: + +```bash +make it # = vendor + fix + stan + test (the full local gate) +``` + +Individually: + +```bash +make fix # rector + php-cs-fixer + prettier (auto-format) +make stan # PHPStan level 8 (phpstan.neon) +make test # PHPUnit +``` + +- PHPStan runs at **level 8** (`phpstan.neon`; `# TODO level up to max`). Do not lower it. Adding an ignore is acceptable only when it mirrors an existing documented pattern in `phpstan.neon` (e.g. cross-Laravel-version generics noise). +- A bug fix without a failing-test-first is not acceptable (CONTRIBUTING.md → Testing). Write the test that reproduces the bug, watch it fail, then fix. +- **The format workflow auto-commits to your branch.** `.github/workflows/format.yml` runs `composer normalize` and Prettier on every push and pushes the result back with `git-auto-commit-action`. After pushing, `git pull` before adding more commits or you will hit a non-fast-forward. + +## Release protocol + +From CONTRIBUTING.md → "Release a New Version": + +1. Review the entries under `CHANGELOG.md` → `## Unreleased`; add any missing ones. +2. Based on those entries and the previous version, decide the next version number (SemVer) and add it to `CHANGELOG.md` (replace `## Unreleased` header with the version, add a fresh empty `## Unreleased` above). +3. Draft a new GitHub release. +4. Use the version number as both git tag and title. +5. Paste the changelog entries as the description. +6. Publish. + +Breaking changes are documented in `UPGRADE.md` and may only ship in a **major** release. +A major release also ends feature work on the previous major (README versioning promise) and requires copying `docs/master` to a new numbered docs dir — see `lighthouse-v7-campaign` for the full major-release runbook. + +Deprecation path: mark the element with a `@deprecated` PHPDoc tag stating the replacement, add a `Deprecated` CHANGELOG entry, keep it working until the next major, then remove it. +Real current examples (`grep -rn "@deprecated" src`): +`src/Console/ClearCacheCommand.php` (`@deprecated in favor of lighthouse:clear-schema-cache`), `src/Auth/CanDirective.php` (`@deprecated TODO remove with v7`), `src/Testing/RefreshesSchemaCache.php`, `src/Testing/MakesGraphQLRequestsLumen.php`. + +## Decision aid: "Is my change breaking?" + +Answer NO to all of these or it is breaking (→ next major, → UPGRADE.md entry): + +- [ ] Printed schema is byte-identical for an unchanged input schema. Verify: `php artisan lighthouse:print-schema` before and after, `diff` the two. Empty diff required for non-breaking work. +- [ ] No `@api`-annotated signature changed. Verify: `grep -n "@api" `. +- [ ] Response JSON shape unchanged for existing queries/mutations/subscriptions. +- [ ] No config default in `src/lighthouse.php` changed in a way that alters behavior for an existing app. +- [ ] No error `message`/`extensions` that a test (or a user) relies on changed. +- [ ] No supported PHP/Laravel version dropped; no dependency floor raised. +- [ ] New required arguments/fields were not added to existing directives or generated types. + +If any box cannot be checked, the change is breaking. Do not "sneak" it into a minor. + +## When NOT to use this skill + +- Actually diagnosing a bug → `lighthouse-debugging-playbook`. +- Turning a report into a failing test → `lighthouse-issue-triage-and-reproduction`. +- Executing a major release end-to-end → `lighthouse-v7-campaign`. +- Proving a change is non-breaking with measurements → `lighthouse-proof-and-analysis-toolkit`. +- The history behind a settled decision → `lighthouse-failure-archaeology`. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e). Re-verify drift with: + +```bash +grep -n "Release a New Version" CONTRIBUTING.md # release steps still present +grep -rc "@api" src # @api surface count (was 33) +grep -rn "@deprecated" src --include="*.php" # deprecation inventory +git show 88d8e18f --stat # pagination-schema revert incident +git log --oneline -3 -- .github/workflows/format.yml # auto-format workflow still active +grep -n "level:" phpstan.neon # PHPStan level (was 8) +``` diff --git a/.claude/skills/lighthouse-config-and-flags/SKILL.md b/.claude/skills/lighthouse-config-and-flags/SKILL.md new file mode 100644 index 000000000..b601aacc0 --- /dev/null +++ b/.claude/skills/lighthouse-config-and-flags/SKILL.md @@ -0,0 +1,128 @@ +--- +name: lighthouse-config-and-flags +description: >- + The catalog of every Lighthouse configuration option and environment variable — + key, default, env var, and whether it is production-ready or experimental. Load + when reading, changing, or documenting a config value; when adding a new config + option; when a bug depends on a flag (schema_cache, query_cache, batchload_relations, + transactional_mutations, force_fill, defer, tracing, subscriptions); or when you + need the exact publish command or env-var name. Includes a checklist for adding an + option without breaking backward compatibility, plus drift-check commands. +--- + +# Lighthouse config and flags + +The single source of truth is `src/lighthouse.php` (547 lines). Users publish it to `config/lighthouse.php`. +Config is read two ways in the codebase: via the `config('lighthouse.…')` helper (e.g. `src/Schema/DirectiveLocator.php:61`) and via an injected `Illuminate\Contracts\Config\Repository` (preferred per the no-Facades rule — the `config()` helper is tolerated). + +Ground truth date: 2026-07-07, v6.68.0 (`master` @ a29ff8e). Every default below was read from `src/lighthouse.php`. Env-var defaults are the fallback in the `env(...)` call. + +## Publishing the config + +```bash +php artisan vendor:publish --tag=lighthouse-config # publishes src/lighthouse.php → config/lighthouse.php +php artisan vendor:publish --tag=lighthouse-schema # publishes default-schema.graphql → your schema_path +``` +Tags verified in `src/LighthouseServiceProvider.php` (`lighthouse-config`, `lighthouse-schema`). + +## Full option catalog + +Legend: **Prod** = stable, safe in production. **Exp** = experimental / handle with care. **Perf** = performance tuning, verify with benchmarks. + +| Key | Default | Env var | Class | Notes | +|---|---|---|---|---| +| `route.uri` | `/graphql` | — | Prod | The endpoint URI. | +| `route.name` | `graphql` | — | Prod | Named route. | +| `route.middleware` | `AcceptJson`, `AttemptAuthentication` (+ commented `EnsureXHR`, `LogGraphQLQueries`) | — | Prod | Runs before execution. `EnsureXHR` is **default-on in v7** (`UPGRADE.md`). | +| `guards` | `null` | — | Prod | Auth guards for `@guard`/`AttemptAuthentication`; `null` = Laravel default. | +| `schema_path` | `base_path('graphql/schema.graphql')` | — | Prod | Root `.graphql` file. | +| `schema_cache.enable` | `env('APP_ENV') !== 'local'` | `LIGHTHOUSE_SCHEMA_CACHE_ENABLE` | Prod/Perf | On by default outside `local`. Stale cache = common trap (`lighthouse-debugging-playbook`). | +| `schema_cache.path` | `bootstrap/cache/lighthouse-schema.php` | `LIGHTHOUSE_SCHEMA_CACHE_PATH` | Prod | Serialized AST/schema file. | +| `cache_directive_tags` | `false` | — | Prod | `@cache` uses a tagged cache; requires a taggable store. | +| `query_cache.enable` | `true` | `LIGHTHOUSE_QUERY_CACHE_ENABLE` | Perf | Caches parsed query strings. | +| `query_cache.mode` | `store` | `LIGHTHOUSE_QUERY_CACHE_MODE` | Perf | `store` (shared cache) / `opcache` (local PHP files) / `hybrid`. Added #2713; hybrid+APQ fixed #2727. | +| `query_cache.opcache_path` | `bootstrap/cache` | `LIGHTHOUSE_QUERY_CACHE_OPCACHE_PATH` | Perf | Folder; one file per query. Only for `opcache`/`hybrid`. | +| `query_cache.store` | `null` | `LIGHTHOUSE_QUERY_CACHE_STORE` | Perf | Cache store; app default if `null`. Not used in `opcache` mode. | +| `query_cache.ttl` | `86400` (24h) | `LIGHTHOUSE_QUERY_CACHE_TTL` | Perf | Seconds; `null` = forever. | +| `validation_cache.enable` | `false` | `LIGHTHOUSE_VALIDATION_CACHE_ENABLE` | Perf/Exp | Caches query validation results. Off by default (#2603). | +| `validation_cache.store` | `null` | `LIGHTHOUSE_VALIDATION_CACHE_STORE` | Perf | | +| `validation_cache.ttl` | `86400` | `LIGHTHOUSE_VALIDATION_CACHE_TTL` | Perf | | +| `parse_source_location` | `true` | — | Perf | `false` drops `locations` from errors; slightly faster. | +| `namespaces.*` | `App\GraphQL\…` per type | — | Prod | Where Lighthouse discovers models, queries, mutations, types, directives, etc. | +| `security.max_query_complexity` | `DISABLED` (0) | — | Prod | `GraphQL\Validator\Rules\QueryComplexity`. | +| `security.max_query_depth` | `DISABLED` (0) | — | Prod | `QueryDepth`. | +| `security.disable_introspection` | `DISABLED` | `LIGHTHOUSE_SECURITY_DISABLE_INTROSPECTION` | Prod | Set env truthy to disable introspection. | +| `pagination.default_count` | `null` | — | Prod | Default page size; `null` = client must ask. | +| `pagination.max_count` | `null` | — | Prod | Max items per page; `null` = unrestricted. | +| `debug` | `INCLUDE_DEBUG_MESSAGE \| INCLUDE_TRACE` | `LIGHTHOUSE_DEBUG` | Prod | Bitmask 0–15; table in the config file. Only applies when Laravel `app.debug=true`. | +| `error_handlers` | Authentication, Authorization, Validation, Reporting | — | Prod | Pipeline; classes implement `Nuwave\Lighthouse\Execution\ErrorHandler`. | +| `field_middleware` | Trim, ConvertEmptyStringsToNull, Sanitize, Validate, TransformArgs, Spread, RenameArgs, DropArgs | — | Prod | **Order is significant** (`lighthouse-architecture-contract`). | +| `global_id_field` | `id` | — | Prod | Node interface global id field name. | +| `persisted_queries` | `true` | — | Prod | Automatic Persisted Queries (Apollo-compatible). | +| `transactional_mutations` | `true` | — | Prod | Wraps model mutations in a transaction committed after the root field; nested-field errors don't roll back. | +| `force_fill` | `true` | — | Prod | `forceFill()` over `fill()` in mutations (GraphQL constrains inputs). | +| `batchload_relations` | `true` | — | Prod | Batch-loads relations to fight N+1. Interacts with #2758/#2679. | +| `shortcut_foreign_key_selection` | `false` | — | Perf/Exp | Only works if every model's PK is exactly `id` (see config comment). Opt-in. | +| `subscriptions.queue_broadcasts` | `true` | `LIGHTHOUSE_QUEUE_BROADCASTS` | Prod | | +| `subscriptions.broadcasts_queue_name` | `null` | `LIGHTHOUSE_BROADCASTS_QUEUE_NAME` | Prod | | +| `subscriptions.storage` | `redis` | `LIGHTHOUSE_SUBSCRIPTION_STORAGE` | Prod | Cache driver for subscription storage. | +| `subscriptions.storage_ttl` | `null` | `LIGHTHOUSE_SUBSCRIPTION_STORAGE_TTL` | Prod | `null` = forever (may leak stale subs). | +| `subscriptions.encrypted_channels` | `false` | `LIGHTHOUSE_SUBSCRIPTION_ENCRYPTED` | Prod | | +| `subscriptions.broadcaster` | `pusher` | `LIGHTHOUSE_BROADCASTER` | Prod | `log`/`echo`/`pusher`/`reverb`. `reverb` uses the `pusher` driver. Use `log` to debug. | +| `subscriptions.exclude_empty` | `true` | `LIGHTHOUSE_SUBSCRIPTION_EXCLUDE_EMPTY` | Prod | | +| `defer.max_nested_fields` | `0` (unlimited) | — | **Exp** | `@defer` is explicitly experimental (config comment). | +| `defer.max_execution_ms` | `0` (unlimited) | — | **Exp** | | +| `federation.entities_resolver_namespace` | `App\GraphQL\Entities` | — | Prod | Apollo Federation `_entities` resolvers. | +| `tracing.driver` | `ApolloTracing::class` | — | Prod | **v7 default changes to `FederatedTracing`** (config comment). | + +## Test-environment variables (NOT library config) + +These configure the test DB/Redis, set in `phpunit.xml.dist` and overridden in `.github/workflows/validate.yml`. They are not part of the published config. + +| Var | phpunit.xml.dist | validate.yml (CI) | +|---|---|---| +| `LIGHTHOUSE_TEST_DB_HOST` | `mysql` | `127.0.0.1` | +| `LIGHTHOUSE_TEST_DB_PORT` | `3306` | `33060` | +| `LIGHTHOUSE_TEST_DB_USERNAME` | `root` | `root` | +| `LIGHTHOUSE_TEST_DB_PASSWORD` | (empty) | `root` | +| `LIGHTHOUSE_TEST_DB_DATABASE` | `test` | `test` | +| `LIGHTHOUSE_TEST_REDIS_HOST` | `redis` | `127.0.0.1` | +| `LIGHTHOUSE_TEST_REDIS_PORT` | `6379` | `63790` | + +## How to add a new config option (checklist) + +1. **Add the key + a comment block** to `src/lighthouse.php`, matching the existing `/*---*/` banner style. Every option is documented inline. +2. **Choose a backward-compatible default.** The default must preserve current behavior for existing apps (change-control rule: no behavioral break in a minor). New behavior ships off/neutral by default (precedent: `validation_cache` off, `shortcut_foreign_key_selection` false). +3. **Decide on an `env()` wrapper.** Pattern in this codebase: deployment-facing toggles get an `env('LIGHTHOUSE_…')` fallback; structural options (namespaces, handler lists) do not. +4. **Read it via injected `ConfigRepository`** in `src/`, not a Facade. +5. **Add a test** that exercises both default and set values. Tests set config in `getEnvironmentSetUp($app)` via `$app->make(ConfigRepository::class)->set('lighthouse.…', …)` — see `tests/DBTestCase.php` and `tests/TestCase.php` for the pattern. +6. **Document** it in `docs/master/` where relevant and add a `CHANGELOG.md` `Added` entry ending with the PR URL. +7. If it changes generated schema or response shape → it may be breaking; run the checklist in `lighthouse-change-control`. + +## Interactions and guards worth flagging + +- `schema_cache.enable` default depends on `APP_ENV` — behaves differently in `local` vs prod. +- `cache_directive_tags=true` requires a **taggable** cache store (Redis/Memcached), not `file`/`array`. +- `query_cache.mode=opcache|hybrid` needs a writable `opcache_path` folder and OPcache enabled. +- `subscriptions.broadcaster=reverb` maps to the `pusher` driver with a `reverb` connection. +- `shortcut_foreign_key_selection=true` is only safe if **every** type's primary key field is literally `id`. +- `security.*` limits are all DISABLED by default — production hardening is opt-in. + +## When NOT to use this skill + +- Why a decision (laziness, transaction semantics) is shaped this way → `lighthouse-architecture-contract`. +- Diagnosing a flag-dependent bug → `lighthouse-debugging-playbook`. +- Domain meaning of a feature (APQ, federation, connections) → `graphql-laravel-reference`. +- Gating a config-default change as breaking → `lighthouse-change-control`. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e). `php -r "require 'src/lighthouse.php'"` does **not** run standalone (it calls `base_path()`), so use grep-based drift checks: + +```bash +grep -nE "^\s*'[a-z_]+' =>" src/lighthouse.php # top-level + nested keys +grep -oE "env\('LIGHTHOUSE_[A-Z_]+'" src/lighthouse.php # env var inventory +grep -n "vendor:publish\|publishes\|lighthouse-config" src/LighthouseServiceProvider.php +grep -rn "env(" src --include="*.php" | grep -v lighthouse.php # env reads outside config (expect none) +``` +`tracing.driver` default and `EnsureXHR`/test-trait behavior change at v7 — re-check `UPGRADE.md` after any major bump. diff --git a/.claude/skills/lighthouse-debugging-playbook/SKILL.md b/.claude/skills/lighthouse-debugging-playbook/SKILL.md new file mode 100644 index 000000000..4f958ef1f --- /dev/null +++ b/.claude/skills/lighthouse-debugging-playbook/SKILL.md @@ -0,0 +1,93 @@ +--- +name: lighthouse-debugging-playbook +description: >- + Triage a live Lighthouse failure fast. Load when a GraphQL request errors, a + schema won't build, a directive "isn't found", results are stale after editing + .graphql files, there are too many DB queries (N+1), subscriptions don't + broadcast, errors are hidden or over-exposed, a request is rejected before + execution, or a test passes locally but fails in CI. Provides a symptom→cause + triage table, the time-wasting traps with their stories, and one-axis-at-a-time + discriminating experiments. For turning a confirmed bug into a failing test, use + lighthouse-issue-triage-and-reproduction instead. +--- + +# Lighthouse debugging playbook + +Fast triage for the failure modes that actually occur in this project. +Every command below is the Docker+Make form (primary) with the native form as an alternative; commands were verified against `Makefile`, `composer.json`, `phpunit.xml.dist`, and source on 2026-07-07, **not** executed here (this authoring environment has no `vendor/`). + +Key definitions used throughout: **directive** = a `@foo` annotation in the schema backed by a `FooDirective` PHP class. **AST** = the parsed schema document Lighthouse manipulates before building the executable schema. **N+1** = one query per parent row instead of one batched query for all of them. + +## First 5 minutes (do this for every report) + +1. Get the exact GraphQL query, variables, and the full error JSON (`errors[].message`, `errors[].extensions`). +2. Get versions: Lighthouse, `webonyx/graphql-php`, PHP, Laravel. Many bugs are version-boundary bugs (see #2771, #2679). +3. Reproduce in a test before theorizing — see `lighthouse-issue-triage-and-reproduction`. A red test beats a paragraph of speculation. +4. Check whether the schema is **cached**: `schema_cache.enable` defaults to `true` when `APP_ENV !== 'local'` (`src/lighthouse.php` ~line 93). A stale cache masquerades as "my change did nothing". +5. Turn on debug detail: set `LIGHTHOUSE_DEBUG` high (see the DebugFlag table in `src/lighthouse.php`, values 0–15) and ensure Laravel `app.debug = true`. + +## Symptom → cause → first move + +| Symptom | Likely cause | First move | +|---|---|---| +| `DefinitionException` / schema won't build | Invalid SDL, or directive manipulating AST wrongly | Read the message; it names the type/field. `src/Exceptions/DefinitionException.php`. Run `lighthouse:validate-schema`. | +| `SchemaSyntaxErrorException` | Malformed `.graphql` | Fix syntax at the reported location. `src/Exceptions/SchemaSyntaxErrorException.php`. | +| "Directive @foo not found" / wrong directive runs | Name→class resolution or namespace priority | `FooDirective` must exist; `DirectiveLocator::className()` = `Str::studly(name).'Directive'`. Check `lighthouse.namespaces.directives`; user namespaces win over Lighthouse's (`DirectiveLocator::namespaces()`). | +| Schema edits have no effect | Schema cache serving stale AST | Clear it: `lighthouse:clear-schema-cache`. In tests, the schema is refreshed via `RefreshesSchemaCache`. | +| Too many DB queries / slow list | Batch loading off, or relation not using a relation directive | Confirm `batchload_relations` (default `true`). Assert query count in a `DBTestCase` (`AssertsQueryCounts`). | +| Batched relation returns error under Laravel 12 | `automaticallyEagerLoadRelationships()` clashes with `RelationBatchLoader` | Known **OPEN #2679**. Workaround: don't enable Laravel auto eager loading with `batchload_relations`. | +| `@canResolved(action: RETURN_VALUE)` ignored | Authorization bypassed when batching relations | Known **OPEN #2758**. Confirm by toggling `batchload_relations=false`. | +| Subscriptions don't broadcast | Broadcaster/driver/storage misconfig | Switch `broadcaster` to `log` (`src/Subscriptions/Broadcasters/LogBroadcaster.php`) to see intended broadcasts in the log; isolates transport vs logic. | +| Errors hidden (no message/trace) | `debug` flag too low, or `app.debug=false` | Raise `LIGHTHOUSE_DEBUG`; debug only applies when Laravel debug is on (`src/lighthouse.php` comment). | +| Internal exception leaked to client | Error handler / debug flag exposing internals | Review `error_handlers` pipeline (`src/Execution/*ErrorHandler.php`) and debug flag; `ClientAware` errors are safe, others are masked unless RETHROW flags set. | +| Request rejected before any resolver runs | HTTP middleware | Inspect `lighthouse.route.middleware`: `AcceptJson`, `AttemptAuthentication`, `EnsureXHR` (`src/Http/Middleware/`). | +| GET or HTML-form POST blocked | `EnsureXHR` middleware | Commented **out** in v6 default config; **default-on in v7** (`UPGRADE.md` → "EnsureXHR is enabled"). If seen in v6, someone enabled it. | +| Passes locally, fails in CI | Matrix cell you didn't run | See "CI-only failures" below. | +| Fatal only under Lumen | Facade/helper used in `src/` | Lumen lacks Facades; `grep -rn "Facades" src` must be empty. Use DI. | + +## Traps that cost real time (with their stories) + +- **The schema cache trap.** Editing a `.graphql` file and seeing no change is almost always a stale schema cache, not a broken change. The cache defaults on outside `local`. In tests, `src/Testing/RefreshesSchemaCache.php` handles refresh (auto in v7). Always clear the cache before concluding your change "doesn't work". + +- **The graphql-php version boundary.** Upstream bumps repeatedly cause subtle breakage. #2771: lazy schema silently becomes eager on `webonyx/graphql-php` ≥ 15.31 (huge latency/memory regression, no error thrown). #2679: Laravel 12's auto eager loading breaks batch loaders. Lesson: when perf or behavior changes with no Lighthouse code change, suspect the dependency version first; bisect by pinning constraints. Lighthouse guards such gaps with `method_exists` checks (real example: `src/GraphQL.php:166`, `// TODO remove this check when updating the required version of webonyx/graphql-php`). + +- **Batch loading vs authorization/eager-loading.** `batchload_relations` changes how relations are fetched and currently interacts badly with `@canResolved` (#2758) and Laravel auto eager loading (#2679). When a relation-related bug appears, toggling `batchload_relations` is the fastest discriminator. + +- **Nested mutation scope.** Nested mutations run unscoped queries (#2744, OPEN) — a mutation can hit records outside the parent relationship. If a user reports "my nested update touched the wrong record", it is likely this known issue, not their misuse. See `lighthouse-failure-archaeology`. + +- **CI-only failures.** `validate.yml` runs the **lowest** dependency set (`--prefer-lowest --prefer-stable`) and old Laravel/PHP, and **removes** some dev deps in CI (`composer remove --dev ... larastan phpstan-mockery phpbench rector`; removes `laravel/pennant` on Laravel 9). So a green local run on highest deps proves nothing about the floor. Reproduce the failing cell's PHP+Laravel+lowest combo locally. + +## Discriminating experiments (toggle ONE axis) + +Change one variable, observe, revert. Each isolates a hypothesis. + +| Hypothesis | Experiment | +|---|---| +| Stale schema cache | `docker compose run --rm php vendor/bin/artisan lighthouse:clear-schema-cache` (native: `php artisan …`) then retry | +| Batch loading is the cause | Set `lighthouse.batchload_relations=false`, retry; if fixed, it's a batch-loader interaction (#2679/#2758) | +| Subscription transport vs logic | Set `lighthouse.subscriptions.broadcaster=log`, retry, read the log | +| Errors are masked, not absent | Raise `LIGHTHOUSE_DEBUG` (e.g. 3 or 15) with `app.debug=true` | +| Schema output changed | `php artisan lighthouse:print-schema` before and after your change, `diff` | +| Isolate one test | `docker compose run --rm php vendor/bin/phpunit --filter=SomeTest` (native: `vendor/bin/phpunit --filter=SomeTest`) | +| Upstream vs Lighthouse regression | Pin `webonyx/graphql-php` to the prior version in `composer.json`, re-measure | + +## When NOT to use this skill + +- Turning a confirmed bug into a failing test / triaging an issue report → `lighthouse-issue-triage-and-reproduction`. +- A battle that's already settled (don't re-diagnose) → `lighthouse-failure-archaeology`. +- Measuring instead of guessing (query counts, benchmarks, tracing) → `lighthouse-diagnostics-and-tooling`. +- Proving a fix is correct/non-breaking → `lighthouse-proof-and-analysis-toolkit`. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e). Re-verify with: + +```bash +ls src/Exceptions/ # exception class inventory +grep -n "className\|studly" src/Schema/DirectiveLocator.php +grep -n "schema_cache\|batchload_relations\|debug" src/lighthouse.php +ls src/Subscriptions/Broadcasters/ # broadcaster drivers (log/echo/pusher) +grep -rh "\$name = 'lighthouse:clear" src/Console/*.php # clear commands +grep -n "method_exists" src/GraphQL.php # upstream version guard example +``` +Issue statuses (#2771, #2679, #2758, #2744) were OPEN on 2026-07-07 — re-check on GitHub before relying on them. diff --git a/.claude/skills/lighthouse-diagnostics-and-tooling/SKILL.md b/.claude/skills/lighthouse-diagnostics-and-tooling/SKILL.md new file mode 100644 index 000000000..d84c895ba --- /dev/null +++ b/.claude/skills/lighthouse-diagnostics-and-tooling/SKILL.md @@ -0,0 +1,117 @@ +--- +name: lighthouse-diagnostics-and-tooling +description: >- + Measure Lighthouse behavior instead of eyeballing it. Load when you need to inspect + or compare the generated schema, validate it, count queries, benchmark performance, + trace a request, read PHPStan output, or profile a regression. Catalogs every + lighthouse:* artisan command with its exact signature, gives measurement recipes + with interpretation guides, and ships runnable scripts (schema-diff, config-keys, + bench-report). For what the results should be (acceptance bar) use + lighthouse-testing-and-qa; for proof strategy use lighthouse-proof-and-analysis-toolkit. +--- + +# Lighthouse diagnostics and tooling + +Rule of the house: **measure, don't guess.** This skill lists the instruments and how to read them. + +Ground truth date: 2026-07-07, v6.68.0 (`master` @ a29ff8e). Command signatures were read from `src/Console/*.php`. Commands shown are the native `php artisan …` form; under Docker prefix with `docker compose run --rm php vendor/bin/artisan …`. + +## Artisan command catalog + +| Command | What it does | Reach for it when | +|---|---|---| +| `lighthouse:print-schema` | Compile and print the schema. Options: `--write`/`-W`, `--disk=`/`-D`, `--json`, `--federation`, `--sort`. | Comparing schema before/after a change (use `--sort` for stable diffs). | +| `lighthouse:validate-schema` | "Validate the GraphQL schema definition." | Confirming SDL + directives build cleanly. | +| `lighthouse:ide-helper` | "Create IDE helper files to improve type checking and autocompletion." | Regenerating directive/type helpers. | +| `lighthouse:cache` | "Compile the GraphQL schema and cache it." | Warming the schema cache in deploys. | +| `lighthouse:clear-schema-cache` | "Clear the GraphQL schema cache." | Schema edits not taking effect. | +| `lighthouse:clear-query-cache` | "Clears the GraphQL query cache." | Stale parsed-query cache. | +| `lighthouse:clear-cache` | **`@deprecated`** in favor of `lighthouse:clear-schema-cache`. | Legacy; don't use in new work. | +| `lighthouse:directive/field/query/mutation/subscription/interface/union/scalar/validator` | Generator stubs. | Scaffolding new code. | + +## Measurement recipes (with interpretation) + +### Schema backward-compatibility check +```bash +php artisan lighthouse:print-schema --sort > before.graphql +# make your change +php artisan lighthouse:clear-schema-cache +php artisan lighthouse:print-schema --sort > after.graphql +diff -u before.graphql after.graphql +``` +Interpretation: **empty diff = non-breaking for consumers** (the hard rule). Any removed field/type, changed nullability, or changed default is breaking. Use `scripts/schema-diff.sh` to do this across two git refs automatically. + +### Schema validity +```bash +php artisan lighthouse:validate-schema +``` +Non-zero exit / thrown `DefinitionException` = your SDL or a directive's AST manipulation is invalid. + +### Query counting (N+1) +Use a `DBTestCase` with `AssertsQueryCounts`: +```php +$this->assertQueryCountMatches($expected, function (): void { + $this->graphQL(/* ... */); +}); +``` +Interpretation: build **more than one** parent row, or batching and N+1 look identical. Real usage: `tests/Integration/Execution/DataLoader/RelationBatchLoaderTest.php`. See `lighthouse-testing-and-qa`. + +### Benchmarks +```bash +make bench # docker: phpbench run --report=aggregate +# native: vendor/bin/phpbench run --report=aggregate +``` +Config: `phpbench.json` (bootstrap `vendor/autoload.php`, path `benchmarks`). Benchmark classes (`@Warmup(1) @Revs(10) @Iterations(10)`): +- `QueryBench` — base for query execution benchmarks. +- `HugeResponseBench` — large response assembly cost. +- `HugeRequestBench` — large request parsing cost. +- `ASTUnserializationBench` — schema-cache AST unserialize cost (relevant to #2771/schema cache work). + +Interpretation discipline: run on the **same machine**, look at the **`rstdev`** (relative standard deviation) column before trusting any delta — a change smaller than rstdev is noise. Baseline first, change, re-run; explain every regression. Use `scripts/bench-report.sh before` / `... after` to capture dated reports. + +### Request tracing +Set `tracing.driver` to `ApolloTracing` (JSON timings per field) or `FederatedTracing` (protobuf; v7 default). Enables per-field resolution timing in the response `extensions`. + +### Query logging +Uncomment `Nuwave\Lighthouse\Http\Middleware\LogGraphQLQueries` in `lighthouse.route.middleware` to log every incoming query. + +### Error verbosity +`LIGHTHOUSE_DEBUG` bitmask 0–15 (table in `src/lighthouse.php`), effective only with Laravel `app.debug=true`. Raise it to surface messages/traces; lower it to reproduce production masking. + +### Profiling a performance regression (the #2771 method) +1. Capture a cachegrind profile (Xdebug) of a minimal operation (e.g. authenticated `{ __typename }`) on the suspect version and the prior version. +2. Compare total PHP work (cachegrind file size / instruction counts) — #2771 saw 9.1 MB → 57 MB. +3. **Ablation:** neutralize the suspected cause (e.g. patch `setTypes(fn () => [])`) and re-measure; if the baseline returns, the cause is confirmed. This ablation + before/after comparison is the gold-standard method — see `lighthouse-proof-and-analysis-toolkit`. + +## Static analysis +```bash +make stan # docker: vendor/bin/phpstan --verbose +``` +`phpstan.neon`: **level 8** (`# TODO level up to max`), bootstraps `phpstan-bootstrap.php`, stubs `_ide_helper.php`, scans `benchmarks/src/tests`. `reportUnmatchedIgnoredErrors: false` because multi-Laravel-version support creates version-specific dead spots. Adding an `ignoreErrors` entry is acceptable only when it mirrors an existing documented pattern (e.g. Laravel generics arity noise). Never lower the level. + +## Shipped scripts (`scripts/`) + +| Script | Purpose | Verified | +|---|---|---| +| `schema-diff.sh ` | Diff printed schema across two git refs; non-empty diff fails. | `bash -n` OK 2026-07-07 (not executed — needs vendor/). | +| `config-keys.php [path]` | Tokenizer-based list of config keys in `src/lighthouse.php` (no Laravel boot). | Executed with PHP 8.4 on 2026-07-07 — prints the keys. | +| `bench-report.sh [label]` | Run PHPBench, tee to a dated `bench-reports/` file. | `bash -n` OK 2026-07-07 (not executed — needs vendor/). | + +## When NOT to use this skill + +- The acceptance bar / what evidence is required → `lighthouse-testing-and-qa`. +- Proof strategy (how to combine tools into airtight evidence) → `lighthouse-proof-and-analysis-toolkit`. +- Triaging a live failure fast → `lighthouse-debugging-playbook`. +- Config values behind these tools → `lighthouse-config-and-flags`. + +## Provenance and maintenance + +Verified 2026-07-07 against v6.68.0 (`master` @ a29ff8e). Re-verify with: + +```bash +grep -rn "lighthouse:" src/Console/*.php | grep -oE "lighthouse:[a-z-]+" | sort -u # command names +sed -n '/protected \$signature/,/SIGNATURE;/p' src/Console/PrintSchemaCommand.php # print-schema options +cat phpbench.json ; ls benchmarks/ +grep -n "level:" phpstan.neon +php .claude/skills/lighthouse-diagnostics-and-tooling/scripts/config-keys.php | wc -l # re-run the shipped script +``` diff --git a/.claude/skills/lighthouse-diagnostics-and-tooling/scripts/bench-report.sh b/.claude/skills/lighthouse-diagnostics-and-tooling/scripts/bench-report.sh new file mode 100755 index 000000000..6cf20b9d5 --- /dev/null +++ b/.claude/skills/lighthouse-diagnostics-and-tooling/scripts/bench-report.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# bench-report.sh — run PHPBench and save a timestamped aggregate report. +# +# Purpose: capture a benchmark baseline before a change and again after, so perf +# claims rest on numbers from the SAME machine. Saves each run to a dated file for +# later comparison. Never trust a delta smaller than the report's rstdev. +# +# Usage: +# scripts/bench-report.sh [label] +# scripts/bench-report.sh before +# scripts/bench-report.sh after +# Output: ./bench-reports/-