


Change a Graph-Based Authorization Service by Writing Tests First
Use when changing a graph-based authorization service. It traces the request path, follows the repo's build targets and lint rules, writes a failing test first, and leaves generated files alone.
To use this AI rule
## Put Correctness First and Use the Repo's Build Targets
Apply project priorities in this order:
1. Correctness: every authorization decision must be accurate.
2. Reliability: the service must remain available and recover predictably.
3. Performance: minimize latency and allocations only after correctness and reliability are protected.
Treat repository-owned build targets as the source of truth. Prefer them over ad hoc commands because they capture required generation, flags, race detection, coverage, linting, and environment setup.
Typical target groups should cover:
- production builds;
- the complete test suite;
- filtered tests for a named case;
- benchmarks with memory metrics;
- integration tests that need containers;
- linting with the repository's configured tool version;
- generated test doubles;
- local development with an in-memory or container-backed datastore.
Before running a target, read its definition and required environment variables. If the canonical target is unavailable or unclear, resolve that gap instead of silently substituting a weaker command.
## Trace a Request Through the Resolver Chain Before Changing It
Map a request through the complete runtime path before changing behavior. Follow this sequence:
1. receive the HTTP or RPC request;
2. apply middleware;
3. enter the transport handler;
4. invoke the command or application service;
5. resolve the authorization graph;
6. access storage;
7. return the response.
Locate the modules that own startup and shutdown, handlers, business commands, graph evaluation, planning, storage interfaces and implementations, model parsing, tuple validation, authentication, and API authorization.
For graph-based checks, inspect the resolver builder and resolver interface together. A resolver pipeline may add caching, dispatch throttling, or shadow comparison only when configuration enables them.
Do not assume the chain is linear. Some resolver systems form a circular delegation chain in which the last resolver returns to the first. Verify construction order, termination conditions, and the difference between optional wrappers and the core local checker before changing dispatch behavior.
Treat runtime wiring as stronger evidence than directory names or design assumptions.
## Follow Lint Naming Rules and Keep Wrapped Errors Matchable
Read the active lint configuration before editing imports or names. Preserve:
- required aliases for generated APIs and parsers;
- the configured import groups and ordering;
- interface names without redundant prefixes;
- constructors named with the pattern `NewTypeName`;
- functional options named with the pattern `WithPropertyName`;
- option types shaped like `TypeNameOption func(*TypeName)`;
- exported constants in the project's exported naming style;
- unexported constants and context keys in the project's local naming style.
Define stable domain errors as sentinel values. Add context without destroying error identity:
- declare a sentinel such as `var ErrNotFound = errors.New("not found")`;
- wrap it with context such as `fmt.Errorf("load relationship: %w", ErrNotFound)`.
Use `errors.Is` or `errors.As` for matching. Do not compare wrapped errors directly, and do not format a wrapped error with a verb that loses the chain.
Keep domain-to-transport conversion at an explicit boundary. Command logic should return domain errors; a dedicated converter should map them to HTTP or RPC responses.
## Write a Failing Test First and Keep Authorization Matrix Tests
Use test-driven development for behavior changes:
1. Add a focused test that expresses the missing or incorrect behavior.
2. Run it and confirm that it fails for the intended reason.
3. Implement the smallest change that can pass it.
4. Run the focused test, then the directly affected suite.
5. Refactor only after the tests are green.
For authorization algorithms, maintain matrix tests that define models, relationship tuples, and expected results for check, object-listing, and user-listing APIs. A matrix stage should make the model and assertions readable together.
A minimal matrix case should preserve this order and meaning:
1. name the test, for example `viewer_can_read_document`;
2. define `user` and `document` types in the stage model;
3. define a `viewer` relation that accepts a user;
4. add the tuple `document:1`, `viewer`, `user:alice`;
5. assert that checking the same object, relation, and user returns `true`.
Keep embedded test assets registered with their runners. When an API has multiple listing or checking surfaces, verify each relevant runner rather than assuming one surface covers the others.
Use benchmarks when changing hot graph paths, resolver selection, storage access, or allocation-sensitive code. Use container-backed tests for datastore-specific behavior.
## Don't Hand-Edit Generated Files or Another Repo's Schemas
Do not edit generated mocks, generated protocol bindings, or other generated artifacts directly. Change the source or generator, then regenerate through the canonical target.
Do not modify externally owned schema definitions in a downstream service repository. Make the schema change in its owning repository, review it there, then update the downstream dependency.
Account for these operational traps:
- a full test target may regenerate mocks automatically, while a direct language-level test command may not;
- integration tests for SQL datastores may require a container runtime;
- resolver recursion may be intentional because the chain is circular;
- lint configuration syntax is version-specific;
- changing a storage interface requires every backend and its shared contract tests to move together.
If a required generator, container runtime, or upstream schema is unavailable, report the missing prerequisite. Do not hand-edit derived output to simulate completion.
## Follow Every Step for New API Endpoints and Storage Changes
For a new API endpoint:
1. update the owned API schema;
2. add the transport handler;
3. add the command or application-service logic;
4. map domain errors at the transport boundary;
5. add focused and matrix tests.
For a storage-interface change:
1. update the interface;
2. update shared storage contract tests;
3. implement the change for every supported backend;
4. run the affected in-memory and container-backed suites.
For a graph-resolver change:
1. implement the resolver interface;
2. add or update builder options;
3. verify conditional composition and circular delegation;
4. test correctness, throttling or caching interactions, and termination;
5. benchmark if the hot path may change.
For a type-system change, update parsing, validation, weighted or optimized graph logic, and the related validation tests as one coherent change.
Before declaring completion, review the diff for accidental generated-file edits, confirm the focused failure became a pass, run the directly affected canonical targets, and record any test class that could not run with its exact prerequisite.
