From 2add515a156ec499e68ffc693ad42adeedadc827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:49:40 +0200 Subject: [PATCH 1/9] Add shared cross-stack guidelines for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/guidelines/shared.md | 362 +++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 .claude/guidelines/shared.md diff --git a/.claude/guidelines/shared.md b/.claude/guidelines/shared.md new file mode 100644 index 0000000000..9bf79a8003 --- /dev/null +++ b/.claude/guidelines/shared.md @@ -0,0 +1,362 @@ +# Probo -- Shared Conventions + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Sections wrapped in are preserved during refresh. + +## Project Overview + +Probo is an open-source compliance platform (SOC-2 and related frameworks). The monorepo contains a Go backend and a TypeScript frontend, built as a single deployable binary (`bin/probod`) that serves both the API and the compiled frontend assets. + +### Repository layout + +| Stack | Language | Directory | Purpose | +|---|---|---|---| +| go-backend | Go 1.26 | `pkg/`, `cmd/`, `e2e/` | API server (GraphQL, MCP), CLI (`prb`), business logic, data access, IAM, background workers | +| typescript-frontend | TypeScript (React 19) | `apps/`, `packages/` | Console admin SPA (`apps/console`), public trust center SPA (`apps/trust`), shared UI and utility packages | + +### Key directories + +- `pkg/probo/` -- core business logic and domain services +- `pkg/coredata/` -- all raw SQL and data access (the only place SQL lives) +- `pkg/server/api/` -- GraphQL APIs (console/v1, connect/v1, trust/v1) and MCP API (mcp/v1) +- `pkg/cmd/` -- CLI commands for the `prb` binary +- `apps/console/` -- admin dashboard (React + Relay + Vite, port 5173) +- `apps/trust/` -- public trust center (React + Relay + Vite, port 5174) +- `packages/` -- shared TypeScript packages (ui, relay, helpers, hooks, eslint-config, etc.) +- `e2e/console/` -- end-to-end tests (Go, run against live `bin/probod`) +- `contrib/claude/` -- detailed subsystem reference guides (15 files) + +### Binaries + +- `bin/probod` -- main server; embeds compiled frontend assets from `apps/console/dist/` and `apps/trust/dist/` +- `bin/prb` -- CLI client +- `bin/probod-bootstrap` -- bootstrap utility + +## Git & Workflow + +### Default branch + +`main`. This is the only long-lived branch. + +### Branching strategy + +Feature branches with the naming convention `/` or `/eng--`. Examples from the actual repository: + +- `SachaProbo/approval-workflow` +- `gearnode/graphql-dataloaders` +- `bryan/eng-136-create-access-review-api` +- `gearnode/magic-link-expiry-error` + +### Commit format + +Free-form imperative mood. Not conventional commits. Titles follow "Verb Noun" pattern. All commits must be signed with `-s` (DCO Signed-off-by) and `-S` (GPG/SSH signature). The commit author must be the human, not an AI assistant. Never add `Co-Authored-By` trailers crediting bots. + +Rules from `contrib/claude/commit.md`: +1. Separate subject from body with a blank line +2. Limit the subject line to 50 characters +3. Capitalize the subject line +4. Do not end the subject line with a period +5. Use the imperative mood in the subject line +6. Wrap the body at 72 characters +7. Use the body to explain what and why, not how + +Examples of actual commits: +- `Add ability to disconnect Slack channel from compliance page` +- `Fix goreleaser snapshot signing creating missing bundle file` +- `Add reusable agent guardrails for prompt injection and data leaks` +- `Rename TruffleHog exclude paths file to plain text` +- `Release v0.154.2` + +### Merge strategy + +Rebase. The history on `main` is fully linear -- no merge commits. PRs may contain multiple commits that land individually on `main`. + +### PR process + +- Platform: GitHub (`getprobo/probo`) +- No PR template file exists +- Review before merge is required (inferred from `reviewDecision=APPROVED` on merged PRs) +- CI checks must pass before merge (see CI/CD Pipeline section below) +- Small team with high-trust review culture (3 active reviewers: gearnode, SachaProbo, codenem) + +### Release process + +- Tag format: `v0.MINOR.PATCH` (project is in 0.x series -- never bump MAJOR) +- Frequency: multiple releases per week, sometimes multiple per day +- Release commit message: exactly `Release v` +- Tag must be annotated: `git tag -a v -m "v"` +- Push both commit and tag: `git push origin main --follow-tags` +- Changelog maintained in `CHANGELOG.md` using Keep-a-Changelog categories (Added, Changed, Fixed, Removed) +- VERSION is tracked in `GNUmakefile` (currently `0.154.2`) +- CI handles binaries, Docker images, npm packages, and attestations on tag push +- Full process documented in `contrib/claude/release.md` + +## CI/CD Pipeline + +All CI is defined in GitHub Actions workflows under `.github/workflows/`. + +### On every PR and push to main (`.github/workflows/make.yaml`) + +| Job | What it does | +|---|---| +| `release-snapshot` | GoReleaser snapshot build + Cosign signing + Trivy image scan (CRITICAL/HIGH fail PR builds) + SBOM generation (Syft/Anchore) + vulnerability scan | +| `build` | `make build` (compiles Go binaries + frontend apps) + uploads artifacts | +| `lint` | `go vet` + `gofmt` + `go fix` + `golangci-lint` (via reviewdog on PRs) + `eslint` on `apps/console`, `apps/trust`, `packages/ui`, `packages/eslint-config` (via reviewdog on PRs) + `n8n-node lint` | +| `test` | `make test` with `-race -cover -coverprofile=coverage.out` + JUnit report + coverage HTML | +| `test-e2e` | Full Docker Compose stack + `make test-e2e` against live `bin/probod` | + +### On tag push (`.github/workflows/release.yaml`) + +- GoReleaser builds `probod`, `probod-bootstrap`, `prb` binaries and multi-arch Docker images +- Docker images published to `ghcr.io/getprobo/probo` +- Cosign signatures for binaries and Docker images +- SBOM attestations and build provenance attestations +- npm package `@probo/n8n-nodes-probo` published to npmjs.org + +### Security scanning + +- **TruffleHog** secret scanning on every push/PR (`.github/workflows/secrets.yaml`) +- **CodeQL** static analysis for Go, TypeScript/JavaScript, and GitHub Actions on every PR + weekly schedule (`.github/workflows/codeql.yml`) +- **Trivy** Docker image scanning for CRITICAL and HIGH vulnerabilities + +### Key CI facts + +- Go version in CI: `1.26.1` (pinned in workflow) +- Node version: read from `.nvmrc` file +- npm version: `11.8.0` (pinned) +- Test runner: `gotestsum` (wrapping `go test`) +- Lint results posted as PR review comments via reviewdog (both golangci-lint and eslint) + +## Monorepo Tooling + +### Build orchestration + +The monorepo uses **two build systems** side by side: + +1. **GNUmakefile** (root) -- orchestrates the full build including both stacks. This is the primary entry point. Key targets: `make build`, `make test`, `make lint`, `make generate`, `make test-e2e`, `make stack-up`/`stack-down`. + +2. **Turborepo** (`turbo.json` + root `package.json`) -- orchestrates TypeScript workspace tasks (build, dev, lint, check, relay). Tasks respect the dependency graph between `apps/*` and `packages/*`. + +### TypeScript workspace + +npm workspaces defined in root `package.json`: +``` +"workspaces": ["apps/*", "packages/*"] +``` + +Turbo tasks: `build`, `dev`, `lint`, `check`, `relay`. + +Key packages consumed across apps: +- `@probo/ui` -- shared component library (Button, Dialog, Table, Icons, Layout) +- `@probo/relay` -- Relay `FetchFunction` factory and typed GraphQL error classes +- `@probo/helpers` -- domain formatters, enum label/variant mappers, utility functions +- `@probo/hooks` -- shared React hooks +- `@probo/eslint-config` -- shared ESLint configuration +- `@probo/routes` -- routing helpers (`loaderFromQueryLoader`, `withQueryRef`) +- `@probo/react-lazy` -- lazy loading utility +- `@probo/i18n` -- internationalization +- `@probo/tsconfig` -- shared TypeScript configuration + +### Go module + +Single Go module: `go.probo.inc/probo` (defined in `go.mod`). No Go workspace or multi-module setup. + +### Code generation + +Code generation is triggered by `go generate` and `npm run relay`. The `make generate` target runs all of them: + +- **GraphQL (gqlgen)**: `go generate ./pkg/server/api/console/v1`, `go generate ./pkg/server/api/connect/v1`, `go generate ./pkg/server/api/trust/v1` +- **MCP (mcpgen)**: `go generate ./pkg/server/api/mcp/v1` +- **Relay compiler**: `npm --workspace @probo/console run relay`, `npm --workspace @probo/trust run relay` + +Generated files that must never be edited by hand: +- `pkg/server/api/*/schema/schema.go` +- `pkg/server/api/*/types/types.go` +- `pkg/server/api/mcp/v1/server/server.go` +- `apps/*/src/__generated__/` (Relay artifacts) + +### Frontend-to-backend build dependency + +The Go binary `bin/probod` embeds the compiled frontend assets. The Makefile declares explicit dependencies: `bin/probod` depends on `apps/console/dist/index.html` and `apps/trust/dist/index.html`. Use `SKIP_APPS=1 make build` to skip frontend compilation for backend-only work. + +## Cross-Stack Contracts + +The GraphQL schema files are the primary contract between the Go backend and TypeScript frontend. There are no Protobuf, OpenAPI, or Swagger specs. + +### GraphQL schemas (Go-authored, TypeScript-consumed) + +| Schema file | Go API package | Frontend consumer | +|---|---|---| +| `pkg/server/api/console/v1/schema.graphql` | `pkg/server/api/console/v1/` | `apps/console` (Relay "core" project) | +| `pkg/server/api/connect/v1/schema.graphql` | `pkg/server/api/connect/v1/` | `apps/console` (Relay "iam" project) | +| `pkg/server/api/trust/v1/schema.graphql` | `pkg/server/api/trust/v1/` | `apps/trust` | + +### How the contract flows + +1. A developer edits a `.graphql` schema file (Go side) +2. `go generate` regenerates the Go resolver stubs and type structs (gqlgen) +3. The developer implements resolver bodies in Go +4. `npm run relay` (Relay compiler) reads the same `.graphql` file and regenerates TypeScript types under `apps/*/src/__generated__/` +5. Frontend components consume the generated types via Relay fragments + +The Relay compiler config files (`apps/console/relay.config.json`, `apps/trust/relay.config.json`) point directly at the Go-side schema files using relative paths (e.g., `"schema": "../../pkg/server/api/trust/v1/schema.graphql"`). + +### MCP specification + +The MCP API is defined in `pkg/server/api/mcp/v1/specification.yaml` (hand-written YAML). This is consumed only by Go code generation (`mcpgen`), not by the TypeScript frontend. + +### Custom scalar type mapping + +Both stacks agree on the same custom GraphQL scalars. The Relay compiler maps them to TypeScript types: + +| GraphQL scalar | Go type | TypeScript type | +|---|---|---| +| `GID` | `gid.GID` (base64url-encoded 192-bit ID) | `string` | +| `Datetime` | `time.Time` | `string` | +| `CursorKey` | custom cursor type | `string` | +| `Duration` | `time.Duration` | `string` | +| `BigInt` | `int64` | `number` | +| `EmailAddr` | `string` (validated) | `string` | + +### GID (Global ID) format + +Every entity in the system is identified by a `gid.GID` -- a 192-bit value encoding tenant ID (8 bytes), entity type (2 bytes), timestamp (8 bytes), and random uniqueness (6 bytes). Serialized as base64url (no padding) across all boundaries: database, GraphQL, JSON, and CLI. Defined in `pkg/gid/gid.go`. Entity type constants registered in `pkg/coredata/entity_type_reg.go` -- removed types are tombstoned with blank identifiers and must never be reused. + +### GraphQL error code contract + +The Go backend returns GraphQL errors with `extensions.code` values. The TypeScript `@probo/relay` package (`packages/relay/src/errors.ts`) maps these codes to typed error classes that frontend error boundaries catch: + +- `UNAUTHENTICATED` -> `UnAuthenticatedError` +- `FORBIDDEN` -> `ForbiddenError` +- `INTERNAL_SERVER_ERROR` -> `InternalServerError` +- `ASSUMPTION_REQUIRED` -> `AssumptionRequiredError` +- `NDA_SIGNATURE_REQUIRED` -> `NDASignatureRequiredError` +- `FULL_NAME_REQUIRED` -> `FullNameRequiredError` + +### Three-interface API surface rule + +Every feature must be exposed through all three interfaces and kept in sync: + +1. **GraphQL** -- `pkg/server/api/console/v1/schema.graphql` (+ codegen) +2. **MCP** -- `pkg/server/api/mcp/v1/specification.yaml` (+ codegen) +3. **CLI** -- `pkg/cmd/` + +If you add a mutation in GraphQL, add the corresponding MCP tool and CLI command. If you rename or change a type, update it everywhere. Every new Go API endpoint must have end-to-end tests in `e2e/`. + +## Deployment + +### Infrastructure + +- **Database**: PostgreSQL 18.1 +- **Object storage**: SeaweedFS (S3-compatible) +- **Docker image**: `ghcr.io/getprobo/probo` (multi-arch, published on tag push) +- **Binaries**: `probod`, `prb`, `probod-bootstrap` (built by GoReleaser) + +### Local development stack + +`compose.yaml` at the repository root defines the local development infrastructure: + +| Service | Purpose | Port | +|---|---|---| +| `postgres` | Primary database | 5432 | +| `seaweedfs` | S3-compatible object storage | 8333 | +| `grafana` | Dashboards | 3001 | +| `prometheus` | Metrics collection | 9191 | +| `loki` | Log aggregation | 3100 | +| `tempo` | Distributed tracing (OTLP) | 4317 | +| `mailpit` | Email testing (SMTP + web UI) | 1025/8025 | +| `chrome` | Headless browser (PDF generation) | 9222 | +| `pebble` | ACME testing (Let's Encrypt simulator) | 14000 | +| `keycloak` | OIDC/SAML identity provider for testing | 8082 | + +Start with `make stack-up`, stop with `make stack-down`. + +### Sandbox development + +For full-stack development and e2e testing, a Lima VM sandbox is available via `contrib/lima/sandbox.sh`. Commands: `make sandbox-create`, `make sandbox-start`, `make sandbox-stop`, `make sandbox-delete`, `make sandbox-ssh`. Code changes are reflected immediately via virtiofs mount. Developer-specific secrets go in `.sandbox.env` (gitignored). + +### Security artifacts + +- Cosign signatures for all binaries and Docker images +- SBOM (CycloneDX JSON) generated by Syft/Anchore on every build +- Build provenance attestations +- License compliance scanning via Trivy + +## Observability + +Observability tooling exists primarily on the Go backend side. The TypeScript frontend does not have its own logging, metrics, or tracing infrastructure. + +### Tracing (Go backend only) + +- OpenTelemetry (`go.opentelemetry.io/otel`) is the tracing framework +- GraphQL resolvers are traced via `pkg/server/gqlutils/tracing.go` +- Traces are exported to Grafana Tempo (OTLP on port 4317 in dev) +- Used across: server, IAM, SCIM, LLM/agent, HTML-to-PDF, and SAML subsystems + +### Logging (Go backend only) + +- Framework: `go.gearno.de/kit/log` -- structured, context-aware, typed fields +- Style: named loggers with `log.String()`, `log.Int()`, etc. field constructors +- Rule: never log PII, PHI, or sensitive data (emails, names, passwords, tokens, health records). Log opaque identifiers (IDs, request IDs) instead. + +### Metrics + +- Prometheus is included in the dev compose stack (port 9191) +- Grafana for dashboards (port 3001) +- Loki for log aggregation (port 3100) + +### Frontend observability + +The TypeScript frontend has no logging, metrics, or tracing libraries. Errors surface to users via Relay typed error classes and React error boundaries. This is a deliberate architectural choice, not a gap to fill -- frontend observability details belong in per-stack guidelines if they evolve. + +## License + +Every source file must start with the ISC license header. This applies to all file types across both stacks: `.go`, `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.sql`, `.graphql`. + +Rules: +- Use the current year for new files +- When editing an existing file with a different year, update to a range (e.g., `2025-2026`) +- Never overwrite the original year -- preserve it in the range +- Comment style varies by file type (`//` for Go/TS/JS, `/* */` for CSS, `--` for SQL, `#` for GraphQL) + +Full header templates in `contrib/claude/license.md`. + +## Review-Enforced Standards + +These patterns are enforced in code review across both stacks. Stack-specific patterns (Go error wrapping, SQL column naming, TypeScript hook deprecations) are documented in their respective per-stack guidelines. + +### Cross-cutting patterns + +- **Three-interface sync rule** -- every feature must be exposed through GraphQL, MCP, and CLI. Adding a mutation in one without updating the others is an approval blocker. (enforced in code review; documented in `CLAUDE.md`) + +- **ISC license headers must be current** -- outdated copyright years are flagged. A bulk update PR (#930, 1572 lines) was merged specifically for this. Reviewers also flag individual files. (enforced in code review, frequency 2+) + +- **GraphQL types implementing Node must have a node resolver** -- declaring `implements Node` in the schema without a corresponding resolver implementation is flagged as incomplete. (enforced in code review) + +- **Access control belongs in ABAC policies, not UI conditionals** -- when a resource's state (e.g., archived) affects allowed operations, enforcement must happen in the IAM policy system (`pkg/probo/policies.go`), not in frontend visibility toggles. Both stacks are affected. (enforced in code review, frequency 3; debated in PR #855) + +- **No panic in resolver code** -- using `panic` in GraphQL resolvers is an approval blocker. A dedicated cleanup PR (#865, 1425 lines) was merged to eliminate all instances. Exception: MCP resolvers use `MustAuthorize()` which deliberately panics, caught by middleware. (enforced in code review, frequency 2) + +- **Simplify over-engineered logic** -- service methods with complex conditional patterns are flagged for simplification. Reviewers call out "useless complex logic" and request straightforward implementations. (enforced in code review, frequency 2) + +### Approval blockers (will block merge) + +These specific violations are called out as "CLAUDE.md violations" in review and will not be approved: + +- Using `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` +- `SQLFragment()` with conditional string building +- Error messages starting with "failed to" instead of "cannot" +- Missing `t.Parallel()` calls in e2e subtests +- Using `panic` in GraphQL resolvers +- Using `withQueryRef` in frontend routes (deprecated -- use page Loader component) +- Multiline function calls with mixed inline/expanded style + +### Review culture + +The team has a high-trust review culture with 3 active reviewers. Most PRs merge with minimal inline comments (61 human comments across 44 PRs in the last year). Many conventions are enforced by reference to documented CLAUDE.md rules rather than lengthy discussion. + + +## Team Notes + +_Reserved for manual additions by the team -- this section is preserved on refresh._ + From b4cc4dcd399915c205ec079b0e0d26a4ef0c6b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:49:45 +0200 Subject: [PATCH 2/9] Add Go backend guidelines for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/guidelines/go-backend/conventions.md | 281 +++++++++++++++ .claude/guidelines/go-backend/index.md | 87 +++++ .../go-backend/module-notes/pkg-cmd.md | 124 +++++++ .../go-backend/module-notes/pkg-coredata.md | 112 ++++++ .../go-backend/module-notes/pkg-llm-agent.md | 137 ++++++++ .../go-backend/module-notes/pkg-server-api.md | 172 ++++++++++ .claude/guidelines/go-backend/patterns.md | 282 ++++++++++++++++ .claude/guidelines/go-backend/pitfalls.md | 240 +++++++++++++ .claude/guidelines/go-backend/testing.md | 319 ++++++++++++++++++ 9 files changed, 1754 insertions(+) create mode 100644 .claude/guidelines/go-backend/conventions.md create mode 100644 .claude/guidelines/go-backend/index.md create mode 100644 .claude/guidelines/go-backend/module-notes/pkg-cmd.md create mode 100644 .claude/guidelines/go-backend/module-notes/pkg-coredata.md create mode 100644 .claude/guidelines/go-backend/module-notes/pkg-llm-agent.md create mode 100644 .claude/guidelines/go-backend/module-notes/pkg-server-api.md create mode 100644 .claude/guidelines/go-backend/patterns.md create mode 100644 .claude/guidelines/go-backend/pitfalls.md create mode 100644 .claude/guidelines/go-backend/testing.md diff --git a/.claude/guidelines/go-backend/conventions.md b/.claude/guidelines/go-backend/conventions.md new file mode 100644 index 0000000000..651ddaf979 --- /dev/null +++ b/.claude/guidelines/go-backend/conventions.md @@ -0,0 +1,281 @@ +# Probo -- Go Backend -- Conventions + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) +> See [shared.md -- Git & Workflow](../shared.md#git--workflow) for commit format, branching, and merge strategy. + +## Go Version + +Go 1.26. Use `new(expr)` to create pointers to values (e.g., `new(1)`, `new("foo")`, `new(time.Now())`) instead of helper functions or temporary variables. + +## Naming Conventions + +### Constructors and types (universal) + +| Convention | Example | +|-----------|---------| +| Constructors: `New*` | `NewService`, `NewServer`, `NewBridge` | +| Config structs: `*Config` | `APIConfig`, `PgConfig`, `TrustCenterConfig` | +| Request structs: `*Request` | `UpdateTrustCenterRequest`, `CreateVendorRequest` | +| Unexported internal types: lowercase | `vendorInfo`, `ctxKey`, `providerInfo` | + +### Receiver names (universal) + +Short single-letter matching the type initial: + +| Receiver | Type | +|----------|------| +| `s` | Service, any service type | +| `c` | Client | +| `p` | Provider | +| `a` | Asset, Agent | +| `v` | Validator, Vendor | +| `f` | Filter | +| `gc` | GarbageCollector | +| `r` | Resolver, Runner | + +### Action string format (universal) + +IAM action strings follow `namespace:resource-type:verb`: + +```go +// See: pkg/probo/actions.go +const ActionVendorCreate = "core:vendor:create" +const ActionVendorUpdate = "core:vendor:update" +const ActionVendorDelete = "core:vendor:delete" +``` + +### SQL column naming (enforced in code review) + +Column names in SQL migrations should not repeat the table prefix. For a `document_version_approvals` table, use `version_id` not `document_version_id`, and `approver_id` not `approver_profile_id`. This was called out in PR #917. + +### Foreign key constraint naming + +Follow the convention `fk_{table}_{referenced_column}`: + +```sql +-- See: pkg/coredata/migrations/ +CONSTRAINT fk_findings_owner FOREIGN KEY (owner_id) REFERENCES ... +``` + +### Domain-specific date fields + +Use meaningful names reflecting domain meaning, not generic timestamp names. Example: `identifiedOn` for when a finding was identified, not `createdAt` (PR #845). + +## Code Style + +### Grouped declarations (universal) + +Use `type ()`, `const ()`, and `var ()` blocks to group related declarations. Using individual `var` statements instead of `var ()` blocks is flagged in review (PR #917, #909). + +```go +// See: pkg/probo/vendor_service.go +type ( + VendorService struct { + svc *TenantService + } + + CreateVendorRequest struct { + OrganizationID gid.GID + Name string + Description *string + } +) +``` + +### String-based enums, not iota (universal, enforced in code review) + +Enum types must use explicit string constants, never `iota`. Reviewers explicitly request replacement when they see iota-based constants (PR #917). + +```go +// See: pkg/coredata/invitation_order_field.go +type InvitationOrderField string + +const ( + InvitationOrderFieldCreatedAt InvitationOrderField = "CREATED_AT" +) +``` + +### One argument per line (universal, enforced in code review) + +A function call is either entirely on one line or fully expanded with one argument per line. Never mix the two styles. This is an **approval blocker**. A dedicated cleanup PR (#866, title "Fix multiline function call style violations") was merged specifically for this. + +```go +// Good -- short enough for one line +id := gid.New(tenantID, "Foo") + +// Good -- fully expanded +svc, err := foo.NewService( + ctx, + db, + logger, + foo.Config{ + Interval: 10 * time.Second, + MaxRetry: 3, + }, +) + +// Bad -- mixed inline and multiline (APPROVAL BLOCKER) +svc, err := foo.NewService(ctx, db, logger, foo.Config{ + Interval: 10 * time.Second, +}) +``` + +### Error message style (universal, enforced in code review) + +Lowercase messages starting with `cannot`. Using `failed to` is an **approval blocker** (cited as "CLAUDE.md violation" in PR #845). + +```go +return nil, fmt.Errorf("cannot load trust center: %w", err) +return nil, fmt.Errorf("cannot create SAML service: %w", err) +``` + +### Compile-time interface satisfaction (universal) + +Verify interface satisfaction at compile time with blank identifier assignments in `var ()` blocks: + +```go +// See: pkg/iam/scim/bridge/provider/googleworkspace/provider.go +var _ provider.Provider = (*Provider)(nil) +``` + +### Context always first parameter (universal) + +Context is always the first parameter. Private struct keys for context values: + +```go +// See: pkg/server/api/authn/context.go +type ctxKey struct{ name string } +var identityKey = &ctxKey{name: "identity"} +``` + +### Pointer literals with new() (universal -- Go 1.26) + +Use `new(expr)` for pointer-to-value literals: + +```go +// See: pkg/trust/compliance_page_service.go +new(s.svc.bucket) +new(organization.Name) +new(0.0) // *float64 pointing to 0.0 +new(10) // *int pointing to 10 +``` + +## Import Ordering + +Two groups separated by a blank line: stdlib, then everything else (third-party and internal sorted together alphabetically). No third blank-line separation between third-party and internal imports. + +```go +// See: pkg/probo/vendor_service.go +import ( + "context" + "fmt" + "time" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/validator" + "go.probo.inc/probo/pkg/webhook" + webhooktypes "go.probo.inc/probo/pkg/webhook/types" +) +``` + +## Project Structure + +### Package layout (universal) + +``` +pkg/ + gid/ # GID types (flat, 2 files) + coredata/ # All SQL, entities, filters (flat, ~100 files) + migrations/ # SQL migration files (YYYYMMDDTHHMMSSZ.sql) + validator/ # Validation framework (flat) + probo/ # Core business logic (~40 service files, flat) + iam/ # IAM root service (flat) + policy/ # Policy evaluator (flat) + saml/ # SAML SP (flat) + oidc/ # OIDC provider (flat) + scim/ # SCIM server + bridge (flat + sub-packages) + bridge/ # Outbound reconciliation + client/ # SCIM HTTP client + provider/ # Provider interface + googleworkspace/ + trust/ # Trust center services (flat) + llm/ # LLM abstraction (flat + provider sub-dirs) + anthropic/ + openai/ + bedrock/ + agent/ # Agent framework (flat) + guardrail/ # Concrete guardrails + agents/ # Domain-specific agents (flat) + server/ # Top-level HTTP server + api/ # API aggregator + authn/ # Authentication middleware + authz/ # Authorization adapter + compliancepage/ # Trust center middleware + console/v1/ # Console GraphQL API + types/ # GQL type mappers + dataloader/ # Batch loaders + schema/ # Generated (do not edit) + connect/v1/ # IAM GraphQL API + types/ + schema/ + trust/v1/ # Trust center GraphQL API + types/ + schema/ + mcp/v1/ # MCP API + types/ + server/ # Generated (do not edit) + files/v1/ # File download handler + gqlutils/ # Shared GraphQL utilities + cmd/ # CLI commands (feature-sliced) + root/ + / + / + cli/ # CLI infrastructure + api/ # GraphQL HTTP client + config/ # YAML config manager +``` + +### File naming (universal) + +- Entity files: `snake_case.go` (one per domain object in coredata and service packages) +- Companion files: `_filter.go`, `_order_field.go`, `_type.go`, `_state.go`, `_status.go` +- Service files: `_service.go` +- Error files: `errors.go` per package +- Test files: `_test.go` co-located + +### Generated files -- never edit (universal) + +| File | Generator | Trigger | +|------|----------|---------| +| `pkg/server/api/*/schema/schema.go` | gqlgen | `go generate ./pkg/server/api/*/v1` | +| `pkg/server/api/*/types/types.go` | gqlgen | `go generate ./pkg/server/api/*/v1` | +| `pkg/server/api/mcp/v1/server/server.go` | mcpgen | `go generate ./pkg/server/api/mcp/v1` | + +## ISC License Header + +Every source file must start with the ISC license header. Use the current year for new files. When editing an existing file with a different year, update to a range (e.g., `2025-2026`). Never overwrite the original year. See [shared.md -- License](../shared.md#license) and `contrib/claude/license.md` for templates. + +Outdated copyright years are flagged in review. A dedicated PR (#930, 1572 lines) was merged specifically for bulk copyright header updates. + +## Review-Enforced Standards + +These Go-specific patterns are enforced in code review. See [shared.md -- Review-Enforced Standards](../shared.md#review-enforced-standards) for cross-cutting patterns. + +### High-confidence patterns (frequency 5+) + +- **Error wrapping with "cannot" prefix** -- using "failed to" or bare `return err` without wrapping are flagged as CLAUDE.md violations. (enforced in code review, frequency 5; PRs #845, #921) + +### Patterns with strong enforcement (frequency 2-4) + +- **var () grouped blocks** -- individual var statements are flagged as style issues. (enforced in code review, frequency 4; PRs #917, #909) +- **One argument per line** -- mixed inline/expanded style is rejected. A dedicated cleanup PR (#866) was merged. (frequency 3; PRs #834, #866) +- **t.Parallel() at all levels** -- missing calls in e2e subtests are flagged as CLAUDE.md violations. (frequency 3; PR #845) +- **JOINs over subqueries** -- subqueries in SELECT are flagged as code smell. (frequency 2; PR #917) +- **String-based enums** -- iota-based enums are rejected. (frequency 2; PR #917) +- **No panic in GraphQL resolvers** -- a cleanup PR (#865, 1425 lines) was merged to eliminate all instances. (frequency 2; PR #865) +- **No speculative indexes** -- indexes without performance justification are rejected. (frequency 2; PR #917) +- **Copyright year current** -- outdated years flagged; bulk update PR #930 merged. (frequency 2; PRs #930, #921) diff --git a/.claude/guidelines/go-backend/index.md b/.claude/guidelines/go-backend/index.md new file mode 100644 index 0000000000..49289d2e35 --- /dev/null +++ b/.claude/guidelines/go-backend/index.md @@ -0,0 +1,87 @@ +# Probo -- Go Backend + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) +> Sections wrapped in will be preserved during refresh. + +## Architecture Overview + +The Go backend is a monolithic service compiled into `bin/probod` that serves all API surfaces (GraphQL, MCP, SCIM, SAML, OIDC), runs background workers (SCIM sync, email sending, webhook delivery, certificate provisioning, evidence description), and embeds the compiled React frontend assets. It is organized as a single Go module (`go.probo.inc/probo`) with a layered architecture enforced by convention rather than framework. + +The layers flow top-down: **HTTP routing** (`pkg/server`) assembles chi routers and middleware chains. **API handlers** (`pkg/server/api/*/v1`) contain GraphQL resolvers (gqlgen), MCP tool resolvers (mcpgen), and protocol handlers (SAML, OIDC, SCIM). **Business logic** (`pkg/probo`, `pkg/iam`, `pkg/trust`) validates requests and orchestrates operations. **Data access** (`pkg/coredata`) is the single location for all raw SQL -- no other package may contain SQL queries. **Infrastructure** packages provide cross-cutting capabilities: LLM integration (`pkg/llm`, `pkg/agent`), background workers (`pkg/webhook`, `pkg/mailer`, `pkg/certmanager`), and security primitives (`pkg/securecookie`, `pkg/securetoken`). + +Every feature must be exposed through all three interfaces (GraphQL, MCP, CLI) and backed by end-to-end tests in `e2e/`. See [shared.md -- Three-interface API surface rule](../shared.md#cross-stack-contracts) for the full contract. + +## Module Map + +| Module | Purpose | Key Patterns | +|--------|---------|-------------| +| `pkg/gid` | 192-bit tenant-scoped entity identifiers | Panic on crypto failure; base64url serialization; entity type registry in coredata | +| `pkg/coredata` | All raw SQL, entity structs, filters, order fields, migrations | Scoper interface, StrictNamedArgs, static SQL fragments, no ORM | +| `pkg/validator` | Fluent validation framework | Check/CheckEach API, SafeText composite, nil = optional | +| `pkg/probo` | Core business logic (40+ domain sub-services) | Service / TenantService two-level tree, request validation, webhook in same tx | +| `pkg/iam` | Authentication and authorization | ABAC policy evaluator, session model, SAML/OIDC/SCIM sub-modules | +| `pkg/iam/policy` | Pure in-memory IAM policy evaluation | Deny-wins precedence, wildcard action matching, ABAC conditions | +| `pkg/iam/saml` | SAML 2.0 SP implementation | Replay detection, GC worker, transaction-wrapped assertion handling | +| `pkg/iam/oidc` | OIDC federated sign-in (Google, Microsoft) | PKCE flow, JWKS caching, enterprise-only enforcement | +| `pkg/iam/scim` | SCIM 2.0 inbound provisioning + outbound bridge | FOR UPDATE SKIP LOCKED, exponential backoff, bridge state machine | +| `pkg/trust` | Public trust center service layer | TenantService scoping, visibility gating, NDA/PDF export | +| `pkg/llm` | Provider-agnostic LLM abstraction | Hexagonal adapters (Anthropic/OpenAI/Bedrock), OTel GenAI tracing | +| `pkg/agent` | LLM agent orchestration framework | Functional options, tool dispatch, guardrails, streaming | +| `pkg/agent/guardrail` | Concrete guardrail implementations | Prompt injection (LLM classifier), sensitive data (regex), system prompt leak | +| `pkg/agents` | Domain-specific LLM agents | Changelog generator, vendor assessor | +| `pkg/server` | Top-level HTTP server and router assembly | chi router, CORS/CSRF/security headers, SPA serving | +| `pkg/server/api/console/v1` | Console GraphQL API (gqlgen) | Schema-first codegen, dataloaders, Relay cursor pagination | +| `pkg/server/api/connect/v1` | IAM GraphQL API + SAML/OIDC/SCIM handlers | Session management, SSO protocol handlers | +| `pkg/server/api/trust/v1` | Trust center GraphQL API | @nda directive, magic link auth, visibility gating | +| `pkg/server/api/mcp/v1` | MCP API (mcpgen) | specification.yaml schema-first, MustAuthorize + panic recovery | +| `pkg/server/api/authn` | Authentication middleware | Session cookie, API key Bearer, identity presence guard | +| `pkg/server/api/authz` | Authorization adapter | AuthorizeFunc closure, IAM-to-GraphQL error mapping | +| `pkg/server/api/compliancepage` | Trust center resolution middleware | SNI routing, slug/GID resolution, member provisioning | +| `pkg/server/api/files/v1` | Public file download handler | S3 presigned URL redirect | +| `pkg/cmd` | `prb` CLI (cobra) | Feature-slice layout, GraphQL queries as const strings | +| `pkg/cli` | CLI infrastructure (GraphQL client, config) | api.Client, Paginate[T], YAML config | +| `e2e` | End-to-end integration tests | Factory builders, RBAC testing, tenant isolation | +| `pkg/` (utilities) | Cross-cutting: webhook, mailer, certmanager, slack, connector, bootstrap | Poll-based workers, HMAC signing, env-driven config | + +## Canonical Examples + +| File | What it demonstrates | +|------|---------------------| +| `pkg/coredata/asset.go` | Complete coredata entity: LoadByID, Insert, Update, Delete, CursorKey, AuthorizationAttributes, Snapshot | +| `pkg/probo/vendor_service.go` | Service layer pattern: Request struct, Validate(), pg.WithTx, webhook in same tx | +| `pkg/server/api/console/v1/v1_resolver.go` | GraphQL resolver pattern: authorize, ProboService, service call, type mapping | +| `pkg/iam/policy/example_test.go` | Policy DSL: Allow/Deny helpers, ABAC conditions, Go example tests | +| `pkg/agent/guardrail/sensitive_data_test.go` | Testing: t.Parallel at all levels, table-driven, require/assert split | +| `e2e/console/vendor_test.go` | E2E test: factory builders, RBAC, tenant isolation, timestamp assertions | + +## Topic Index + +- [Patterns](./patterns.md) -- error handling, data access, authorization, DI, observability +- [Conventions](./conventions.md) -- naming, code style, imports, project structure +- [Testing](./testing.md) -- framework, organization, naming, utilities, example test +- [Pitfalls](./pitfalls.md) -- stack-specific pitfalls and anti-patterns + +## Module Notes + +Modules with distinctive patterns that differ from stack-wide conventions: + +- [pkg/coredata](./module-notes/pkg-coredata.md) -- data access layer specifics +- [pkg/server/api](./module-notes/pkg-server-api.md) -- GraphQL/MCP resolver patterns +- [pkg/cmd](./module-notes/pkg-cmd.md) -- CLI command structure +- [pkg/llm and pkg/agent](./module-notes/pkg-llm-agent.md) -- LLM integration patterns + + +## Team Notes + +_Reserved for manual additions by the team -- this section is preserved on refresh._ + + + +## Open Questions + +- `pkg/coredata/asset_filter.go` uses `pgx.NamedArgs` while `vendor_filter.go` uses `pgx.StrictNamedArgs` -- is this a migration in progress toward StrictNamedArgs everywhere? +- `pkg/trust/service.go` declares a `proboSvc` field on both `Service` and `TenantService` but `NewService` never accepts it as a parameter -- is this a wiring oversight? +- Several modules have no unit tests (pkg/iam/oidc, pkg/trust, pkg/server/api/authn) -- is coverage exclusively via e2e tests, or are gaps to be filled? +- The `Vendor` struct in coredata has a `TenantID` field unlike all other entities -- is this intentional legacy or should it be removed? + diff --git a/.claude/guidelines/go-backend/module-notes/pkg-cmd.md b/.claude/guidelines/go-backend/module-notes/pkg-cmd.md new file mode 100644 index 0000000000..d993ca78d1 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/pkg-cmd.md @@ -0,0 +1,124 @@ +# Probo -- Go Backend -- pkg/cmd (CLI) + +> Module-specific patterns that differ from stack-wide conventions. +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). + +## Purpose + +Implements the `prb` CLI tool using cobra. All commands communicate with the Probo backend exclusively via GraphQL over HTTPS. No direct database access. + +## Directory Structure (feature-sliced) + +Unlike the flat package pattern used elsewhere in the backend, the CLI uses a feature-slice layout: + +``` +pkg/cmd/ + root/root.go # Registers all top-level resource groups + / + .go # Group command, aggregates verb sub-commands + / + .go # Leaf command, owns RunE logic +``` + +Example: `pkg/cmd/vendor/vendor.go` groups `list/`, `create/`, `view/`, `update/`, `delete/`. + +## Command Construction Pattern + +Every leaf command receives `*cmdutil.Factory` as its sole dependency: + +```go +// See pattern from: contrib/claude/cli.md +func NewCmdCreateVendor(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a vendor", + RunE: func(cmd *cobra.Command, args []string) error { + // 1. Load config + cfg, err := f.Config() + if err != nil { return err } + + // 2. Resolve host + token + hc, err := cfg.DefaultHost() + if err != nil { return err } + + // 3. Resolve organization + orgID := cmd.Flag("org").Value.String() + if orgID == "" { orgID = hc.Organization } + + // 4. Create API client + client := api.NewClient(hc.Host, hc.Token, "/api/console/v1/graphql", 0) + + // 5. Execute GraphQL + result, err := client.Do(cmd.Context(), query, variables) + + // 6. Print output + fmt.Fprintln(f.IOStreams.Out, "Vendor created:", id) + return nil + }, + } + return cmd +} +``` + +## GraphQL Queries as Constants + +GraphQL queries are defined as `const` strings at the package level in leaf command files. Response types are unexported structs local to the leaf package: + +```go +// See pattern from: contrib/claude/cli.md +const createVendorMutation = ` + mutation CreateVendor($input: CreateVendorInput!) { + createVendor(input: $input) { + vendor { id name } + } + } +` + +type createVendorResponse struct { + CreateVendor struct { + Vendor struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"vendor"` + } `json:"createVendor"` +} +``` + +## Flag Conventions + +- Flag naming: kebab-case (`--order-by`, `--inherent-likelihood`) +- Standard short flags: `-L` (limit), `-o` (output), `-q` (query), `-y` (yes) +- Organization resolution: `--org` flag first, then `hc.Organization` from config +- Update commands: only include fields where `cmd.Flags().Changed()` is true +- Delete commands: require `--yes` flag or interactive `huh.NewConfirm()` prompt +- List commands: support `--output json|table` (default is table) + +## Output Formatting + +- Table output via `cmdutil.NewTable` (pre-styled lipgloss/table) +- JSON output via `cmdutil.PrintJSON` +- Truncation message goes to stderr, not stdout +- Single confirmation line for create/update/delete + +## Pagination + +List commands use `api.Paginate[T]` with `--limit` / `-L` flag (default 30): + +```go +// See: pkg/cli/api/pagination.go +nodes, totalCount, err := api.Paginate[vendorNode]( + cmd.Context(), + client, + listVendorsQuery, + variables, + func(raw json.RawMessage) (*api.Connection[vendorNode], error) { ... }, +) +``` + +## Interactive Prompts + +When `f.IOStreams.IsInteractive()` is true, commands use `charmbracelet/huh` for prompts. Non-interactive mode (piped input) skips prompts and requires all data via flags. + +## Registration + +New resource group commands must be registered in `pkg/cmd/root/root.go`. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-coredata.md b/.claude/guidelines/go-backend/module-notes/pkg-coredata.md new file mode 100644 index 0000000000..54719906d0 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/pkg-coredata.md @@ -0,0 +1,112 @@ +# Probo -- Go Backend -- pkg/coredata + +> Module-specific patterns that differ from stack-wide conventions. +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). + +## Purpose + +All raw SQL queries, entity struct definitions, filters, order fields, enumerations, and database migrations for every domain object. No ORM -- all SQL is hand-written inline using pgx named arguments. This is the **single source of truth** for database interaction. + +## Entity File Pattern + +One file per entity in snake_case (e.g., `asset.go`, `vendor_risk_assessment.go`). Companion files use suffixes: `_filter.go`, `_order_field.go`, `_type.go`, `_state.go`, `_status.go`. + +Every entity struct uses: +- `gid.GID` for primary key +- `db` struct tags for pgx column mapping +- `time.Time` for CreatedAt/UpdatedAt +- Pointer types for nullable columns +- **No TenantID field** (injected by Scoper at query time) + +```go +// See: pkg/coredata/asset.go +type Asset struct { + ID gid.GID `db:"id"` + SnapshotID *gid.GID `db:"snapshot_id"` + Name string `db:"name"` + OwnerID gid.GID `db:"owner_profile_id"` + OrganizationID gid.GID `db:"organization_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} +``` + +## Method Signatures + +| Method | Receiver | Returns | Notes | +|--------|----------|---------|-------| +| `LoadByID` | `*Entity` | `error` | Assigns into receiver via `*e = entity` | +| `LoadAllBy*` | `*Entities` (slice) | `error` | Paginated with `page.Cursor[OrderField]` | +| `CountBy*` | `*Entities` | `(int, error)` | Uses `COUNT(id)` | +| `Insert` | `*Entity` | `error` | Uses `scope.GetTenantID()` for tenant_id | +| `Update` | `*Entity` | `error` | Uses `RETURNING` and reassigns receiver | +| `Delete` | `*Entity` | `error` | Checks `snapshot_id IS NULL` to prevent deleting snapshots | +| `CursorKey` | `*Entity` | `page.CursorKey` | Switch on OrderField; panics on unknown | +| `AuthorizationAttributes` | `*Entity` | `(map[string]string, error)` | Returns org/tenant IDs for ABAC | + +## Row Collection + +```go +// Single row -- See: pkg/coredata/asset.go +asset, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Asset]) + +// Multiple rows +assets, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Asset]) + +// Map pgx.ErrNoRows to sentinel +if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound +} +``` + +## Filter Pattern (double-pointer three-state logic) + +Filter fields use double pointers for three-state logic: +- `nil` = no filter +- `*nil` = IS NULL +- `*val` = equals + +```go +// See: pkg/coredata/vendor_filter.go +func (f *VendorFilter) SQLArguments() pgx.StrictNamedArgs { + args := pgx.StrictNamedArgs{ + "show_on_trust_center": nil, // always declared + "has_snapshot_filter": false, // always declared + "filter_snapshot_id": nil, // always declared + } + if f.showOnTrustCenter != nil { + args["show_on_trust_center"] = *f.showOnTrustCenter + } + return args +} +``` + +## OrderField Pattern (complete version) + +Newer order fields implement the full set: `Column()`, `IsValid()`, `String()`, `MarshalText()`, `UnmarshalText()`. Follow the `InvitationOrderField` pattern in `pkg/coredata/invitation_order_field.go` for new entities. + +Some older order fields (e.g., `AssetOrderField`) only implement `Column()` and `String()`. This is an inconsistency -- new entities should use the complete pattern. + +## Migration Rules + +- Files in `pkg/coredata/migrations/` named `YYYYMMDDTHHMMSSZ.sql` (UTC timestamps) +- One logical change per file +- No indexes by default (only when justified by observed latency) +- No DEFAULT clauses on new tables +- When adding non-nullable columns to existing tables: use DEFAULT to backfill, then drop DEFAULT in the same migration +- ON DELETE CASCADE for audit log FKs to organizations + +## Entity Type Registry + +`entity_type_reg.go` contains all entity type uint16 constants. Critical rules: +- Never reuse removed type numbers (tombstone with `_ uint16 = N // Removed`) +- Always append new types at the end +- Every new entity must have a case in the `NewEntityFromID` switch + +## No Tests in This Package + +`pkg/coredata` has no unit tests. Coverage comes from e2e tests in `e2e/console/`. This is intentional -- the package is pure data access with no business logic worth unit-testing in isolation. + +## No Logging + +The data access layer does not log. Errors are returned to callers who log at the service layer. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-llm-agent.md b/.claude/guidelines/go-backend/module-notes/pkg-llm-agent.md new file mode 100644 index 0000000000..66b7a6aa4c --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/pkg-llm-agent.md @@ -0,0 +1,137 @@ +# Probo -- Go Backend -- pkg/llm and pkg/agent (LLM Integration) + +> Module-specific patterns that differ from stack-wide conventions. +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). + +## Architecture + +The LLM integration is split into three layers: + +1. **pkg/llm** -- Provider-agnostic LLM abstraction (hexagonal pattern, unique in this codebase) +2. **pkg/agent** -- LLM agent orchestration framework (tool dispatch, guardrails, streaming) +3. **pkg/agents** -- Domain-specific agent implementations (changelog, vendor assessment) + +## pkg/llm -- Provider Abstraction + +### Hexagonal architecture (unique in codebase) + +Unlike the flat pattern used elsewhere, `pkg/llm` uses a hexagonal (ports-and-adapters) architecture: + +- **Core types** (root package): `Provider` interface, `ChatCompletionRequest/Response`, `Message`, `Part`, `Tool`, error types +- **Adapters** (sub-packages): `anthropic/`, `openai/`, `bedrock/` each implement `Provider` +- **Instrumented client** (root package): `Client` wraps any `Provider` with logging and OTel tracing + +### Provider interface + +```go +// See: pkg/llm/provider.go +type Provider interface { + ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) + ChatCompletionStream(ctx context.Context, req ChatCompletionRequest) (ChatCompletionStream, error) +} +``` + +### Canonical error types + +All provider adapters map vendor-specific errors to four canonical types: + +| Error type | Meaning | +|-----------|---------| +| `ErrRateLimit` | API rate limit exceeded | +| `ErrContextLength` | Token limit exceeded | +| `ErrContentFilter` | Content policy violation | +| `ErrAuthentication` | Invalid API credentials | + +### OTel GenAI semantic conventions + +LLM calls are traced with GenAI semantic conventions (semconv v1.37.0). Spans named `chat {model}` with attributes like `gen_ai.operation.name`, `gen_ai.usage.input_tokens`, etc. No PII or message content is ever logged -- only model name, token counts, and duration. + +### Streaming -- always close + +Streaming calls must always call `stream.Close()` even on early exit. The OTel span is only finalized by `Next()` returning false or `Close()`. Forgetting `Close()` leaks the span. + +### Provider-specific gotchas + +- **Anthropic** requires `MaxTokens` to be set (returns `ErrContextLength` if nil) +- **Bedrock** does not support `ToolChoiceNone` (tools are silently omitted) +- **Bedrock** silently drops `FilePart` and `ImagePart` in user messages +- **OpenAI** is the only adapter supporting `ResponseFormat` (JSON schema mode) + +## pkg/agent -- Agent Framework + +### Construction via functional options + +Agents are configured declaratively: + +```go +// See: pkg/agent/agent.go +agent := agent.New( + "my-agent", + "You are a helpful assistant.", + llmClient, + agent.WithTools(myTool), + agent.WithLogger(logger), + agent.WithModel("claude-sonnet-4-20250514"), + agent.WithInputGuardrails(promptInjectionGuard), + agent.WithOutputGuardrails(sensitiveDataGuard), +) +``` + +### Default logger discards output + +The agent's default logger writes to `io.Discard`. You must pass `WithLogger(myLogger)` to see any diagnostics. This is different from service packages where loggers are always wired. + +### Tool interface + +Tools implement a two-tier interface: `ToolDescriptor` (name + JSON schema) and `Tool` (extends with `Execute`). `FunctionTool[P]` is a generic constructor that auto-generates JSON schema from the parameter type: + +```go +// See: pkg/agent/tool.go +tool := agent.FunctionTool("search", "Search the web", func(ctx context.Context, params SearchParams) (agent.ToolResult, error) { + // ... +}) +``` + +### Guardrails + +- **Input guardrails** check messages before LLM call (e.g., `PromptInjectionGuardrail`) +- **Output guardrails** check each assistant message (e.g., `SensitiveDataGuardrail`, `SystemPromptLeakGuardrail`) +- `PromptInjectionGuardrail` **fails open** on classifier error (defense-in-depth philosophy) +- `SensitiveDataGuardrail` has broad patterns (`select `, `update `) that may cause false positives + +### Approval / human-in-the-loop + +Tool calls matching `ApprovalConfig` raise `InterruptedError`. Runs must be resumed via `Resume()`, not re-run via `Run()`. The `InterruptedError` carries opaque state that cannot be reconstructed. + +### Streaming events + +Events are sent on a buffered channel (size 64). If the consumer reads too slowly, events are **dropped silently** -- `trySendEvent` does not block. + +## pkg/agents -- Domain Agents + +Thin facades over `pkg/agent`: + +- `GenerateChangelog(ctx, oldContent, newContent)` -- single-turn text diff summary +- `AssessVendor(ctx, websiteURL)` -- structured vendor info via `RunTyped[vendorInfo]` + +Each method creates a new `agent.Agent` per call (stateless, no session). System prompts are unexported `const` strings. + +**Known issue:** The logger passed to `NewAgent` is stored but never forwarded to inner `agent.New` calls -- inner agents default to `io.Discard` logging. + +## Testing Pattern + +LLM and agent tests use mock implementations rather than live API calls: + +```go +// See: pkg/llm/llm_test.go +type mockProvider struct { + chatCompletionFunc func(...) (*llm.ChatCompletionResponse, error) +} + +// See: pkg/agent/agent_test.go +// mockProvider with pre-scripted responses +// mockChatStream for streaming tests +// recordingHook for verifying hook invocations +``` + +No per-provider integration tests exist. The `pkg/agents` package has no tests at all. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-server-api.md b/.claude/guidelines/go-backend/module-notes/pkg-server-api.md new file mode 100644 index 0000000000..9e791c63b5 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/pkg-server-api.md @@ -0,0 +1,172 @@ +# Probo -- Go Backend -- pkg/server/api (GraphQL, MCP, Protocol Handlers) + +> Module-specific patterns that differ from stack-wide conventions. +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). + +## Three API Surfaces + +The server exposes three API surfaces under `/api/`: + +| API | Path | Auth | Schema source | Codegen | +|-----|------|------|--------------|---------| +| Console GraphQL | `/api/console/v1/graphql` | Session cookie or API key | `schema.graphql` | `go generate ./pkg/server/api/console/v1` | +| Connect GraphQL | `/api/connect/v1/graphql` | Session cookie or API key | `schema.graphql` | `go generate ./pkg/server/api/connect/v1` | +| Trust GraphQL | `/trust/{slug}/api/trust/v1/graphql` | Session cookie (optional) | `schema.graphql` | `go generate ./pkg/server/api/trust/v1` | +| MCP | `/mcp/v1` | API key only | `specification.yaml` | `go generate ./pkg/server/api/mcp/v1` | + +Every feature must be exposed through GraphQL + MCP + CLI. See [shared.md -- Three-interface API surface rule](../shared.md#cross-stack-contracts). + +## GraphQL Resolver Pattern (console/v1, connect/v1, trust/v1) + +### Schema-first with gqlgen + +GraphQL APIs use schema-first code generation via gqlgen. The schema file (`schema.graphql`) is hand-written. Running `go generate` produces: + +- `schema/schema.go` -- executable schema (never edit) +- `types/types.go` -- type struct definitions (never edit) +- `v1_resolver.go` -- resolver stubs (hand-edit method bodies only) + +### Resolver method sequence + +Every resolver follows this strict sequence: + +```go +// See: pkg/server/api/console/v1/v1_resolver.go +func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.CreateVendorPayload, error) { + // 1. Authorize + if err := r.authorize(ctx, input.OrganizationID, probo.ActionVendorCreate); err != nil { + return nil, err + } + + // 2. Get tenant service + prb := r.ProboService(ctx, input.OrganizationID.TenantID()) + + // 3. Call service method + vendor, err := prb.Vendors.Create(ctx, &probo.CreateVendorRequest{...}) + + // 4. Handle errors + map to types + if err != nil { + r.logger.ErrorCtx(ctx, "cannot create vendor", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + return &types.CreateVendorPayload{Vendor: types.NewVendor(vendor)}, nil +} +``` + +### Connection types (Relay cursor pagination) + +Each paginated entity has a `*Connection` struct in `types/` with: +- Edges, PageInfo +- Resolver (parent type for TotalCount dispatch) +- ParentID (gid.GID) +- Filter (optional) + +```go +// See: pkg/server/api/console/v1/types/vendor.go +type VendorConnection struct { + Resolver any + ParentID gid.GID + Filter *coredata.VendorFilter +} +``` + +TotalCount resolvers dispatch on `obj.Resolver.(type)` to handle different parent contexts. Adding a new parent requires updating the type switch. + +### Dataloaders (console/v1 only) + +Per-request batch loaders solve N+1 queries for 11 entity types. Injected via HTTP middleware. + +```go +// See: pkg/server/api/console/v1/dataloader/dataloader.go +loaders := dataloader.FromContext(ctx) +org, err := loaders.Organization.Load(ctx, vendor.OrganizationID) +``` + +Always use dataloaders for entities that have them. Direct service calls cause N+1 queries. + +### Custom scalars + +| GraphQL scalar | Go type | Adapter | +|---|---|---| +| `GID` | `gid.GID` | `gqlutils/types/gid` | +| `Datetime` | `time.Time` | stdlib `time.Time` | +| `CursorKey` | `page.CursorKey` | `gqlutils/types/cursorkey` | +| `Duration` | `time.Duration` | stdlib | +| `BigInt` | `int64` | stdlib | +| `EmailAddr` | `mail.Addr` | `gqlutils/types/addr` | + +### Error helpers (gqlutils) + +All resolver errors must use typed gqlutils helpers, never raw `gqlerror.Error` construction: + +| Helper | Extension code | When to use | +|--------|---------------|-------------| +| `Internal(ctx)` | `INTERNAL_SERVER_ERROR` | Unexpected errors (hides details) | +| `NotFoundf(ctx, ...)` | `NOT_FOUND` | Resource not found | +| `Forbidden(ctx, msg)` | `FORBIDDEN` | Permission denied | +| `Invalidf(ctx, ...)` | `INVALID` | Validation failure | +| `Unauthenticatedf(ctx, ...)` | `UNAUTHENTICATED` | Missing auth | +| `AssumptionRequired(ctx)` | `ASSUMPTION_REQUIRED` | Org session not assumed | +| `NDASignatureRequiredf(ctx, ...)` | `NDA_SIGNATURE_REQUIRED` | Trust center NDA required | + +## MCP Resolver Pattern + +### Schema-first with mcpgen + +MCP tools are defined in `specification.yaml` (hand-written YAML). Running `go generate` produces `server/server.go` and `types/types.go`. + +### Resolver differences from GraphQL + +1. **Authorization uses panic**: `r.MustAuthorize(ctx, id, action)` panics on failure. `RecoveryMiddleware` catches and translates to MCP error. +2. **First return is always nil**: `return nil, output, nil` +3. **API key auth only**: No session cookies. +4. **Type helpers in types/**: One file per entity with `New*` conversion functions (similar to GraphQL types/). +5. **Optional fields use Omittable**: `mcpgen/omittable` type for nullable update fields. Unwrap with `UnwrapOmittable` helper. + +```go +// See: pkg/server/api/mcp/v1/schema.resolvers.go +func (r *Resolver) UpdateVendor(ctx context.Context, req mcp.CallToolRequest, input types.UpdateVendorInput) (*mcp.CallToolResult, *types.UpdateVendorOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionVendorUpdate) + // ... + return nil, types.NewUpdateVendorOutput(vendor), nil +} +``` + +## Authentication Middleware Chain + +The middleware chain order is critical (session -> API key -> identity presence): + +```go +// See: pkg/server/api/console/v1/resolver.go +mux.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig)) +mux.Use(authn.NewAPIKeyMiddleware(iamSvc, tokenSecret)) +mux.Use(authn.NewIdentityPresenceMiddleware()) +``` + +- Session and API key are mutually exclusive (checked symmetrically) +- Identity presence must be last (only checks context, does not authenticate) +- Unexpected errors in middleware panic (caught by upstream recovery) + +## Trust Center GraphQL (@nda directive) + +The trust API has a unique `@nda` directive that gates field resolution behind completed electronic signatures: + +```graphql +# See: pkg/server/api/trust/v1/schema.graphql +type Document implements Node @nda { ... } +``` + +The directive handler checks NDA completion at field resolution time. New types with NDA-gated content must have `@nda` applied on the type or each field individually. + +## Connect API (SAML/OIDC/SCIM handlers) + +The connect/v1 module mounts protocol handlers alongside its GraphQL API: + +| Endpoint | Handler | Protocol | +|----------|---------|----------| +| `/graphql` | gqlgen | GraphQL | +| `/saml/2.0/*` | SAMLHandler | SAML 2.0 SP | +| `/oidc/*/*` | OIDCHandler | OIDC/OAuth2 | +| `/scim/2.0/*` | SCIMHandler | SCIM 2.0 | + +SAML and OIDC handlers bypass GraphQL entirely -- they call IAM services directly, set secure cookies, and issue HTTP redirects. diff --git a/.claude/guidelines/go-backend/patterns.md b/.claude/guidelines/go-backend/patterns.md new file mode 100644 index 0000000000..b54fd3977d --- /dev/null +++ b/.claude/guidelines/go-backend/patterns.md @@ -0,0 +1,282 @@ +# Probo -- Go Backend -- Patterns + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +## Code Organization + +### Two-level service tree (universal) + +Every domain service follows a two-level pattern: a global `Service` singleton and a tenant-scoped `TenantService` created via `Service.WithTenant(tenantID)`. + +The global `Service` holds infrastructure dependencies (pg.Client, S3, LLM, logger). `TenantService` bundles a `coredata.Scoper` for tenant isolation and exposes all domain sub-services as public fields. + +``` +// See: pkg/probo/service.go +Service (global) -> WithTenant(tenantID) -> TenantService + .Vendors VendorService + .Documents DocumentService + .Frameworks FrameworkService + ... (~40 sub-services) +``` + +Sub-services are thin structs with a single `svc *TenantService` field. Each corresponds to one file named `_service.go`. + +This pattern is used universally across `pkg/probo`, `pkg/iam`, and `pkg/trust`. + +### Request struct + Validate() (universal) + +Every mutating service method takes a typed `*Request` struct with a `Validate() error` method that uses the fluent `pkg/validator` API. Validation is always the first call inside the service method body. + +```go +// See: pkg/probo/vendor_service.go +func (r *CreateVendorRequest) Validate() error { + v := validator.New() + v.Check(r.Name, "name", validator.SafeText(NameMaxLength)) + v.Check(r.WebsiteURL, "websiteURL", validator.HTTPSUrl()) + return v.Error() +} +``` + +### All SQL in pkg/coredata only (universal) + +This is the most fundamental architectural constraint. See [shared.md -- Architecture Decisions](../shared.md#cross-stack-contracts) for the project-wide rule. + +All raw SQL lives exclusively in `pkg/coredata`. Service packages (`pkg/probo`, `pkg/iam`, `pkg/trust`) call coredata model methods (LoadByID, Insert, Update, Delete) inside `pg.WithConn` or `pg.WithTx` closures. No other package may contain SQL queries. + +## Error Handling + +### Error wrapping convention (universal, enforced in code review) + +All errors are wrapped with `fmt.Errorf` using lowercase messages starting with `cannot`: + +```go +// See: pkg/coredata/custom_domain.go +return nil, fmt.Errorf("cannot load custom domain: %w", err) +``` + +Using "failed to" or other prefixes is an **approval blocker** in code review. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). + +### Sentinel errors (universal -- pkg/coredata) + +Three sentinel errors defined in `pkg/coredata/errors.go`: + +| Sentinel | Mapped from | Usage | +|----------|------------|-------| +| `ErrResourceNotFound` | `pgx.ErrNoRows` | Resource lookup failures | +| `ErrResourceAlreadyExists` | PostgreSQL error code 23505 (unique violation) | Insert conflicts | +| `ErrResourceInUse` | PostgreSQL FK constraint violation | Deletion blocked by references | + +Service packages map these to domain-specific typed errors: + +```go +// See: pkg/iam/auth_service.go +if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, NewErrIdentityNotFound(email) +} +``` + +### Custom typed error structs (universal -- service packages) + +Domain error types are constructed via `New*Error()` factory functions and collected in `errors.go` per package. Each type implements `Error() string` and optionally `Unwrap() error`. + +| Package | Error file | Example types | +|---------|-----------|--------------| +| `pkg/iam` | `errors.go` | ErrInvalidCredentials, ErrSessionExpired, ErrAssumptionRequired | +| `pkg/iam/saml` | `errors.go` | ErrSAMLConfigurationNotFound, ErrReplayAttackDetected | +| `pkg/iam/oidc` | `errors.go` | ErrProviderNotEnabled, ErrPersonalAccountNotAllowed | +| `pkg/trust` | `errors.go` | ErrPageNotFound, ErrDocumentNotVisible | + +Callers use `errors.Is` / `errors.As` for type checks -- never string comparison. + +### GraphQL error mapping (universal -- resolver packages) + +GraphQL resolvers use `gqlutils` helper functions from `pkg/server/gqlutils/errors.go`: + +| Helper | When to use | +|--------|------------| +| `gqlutils.Internal(ctx)` | Unexpected errors (hides details from client, logs internally) | +| `gqlutils.NotFoundf(ctx, msg, ...)` | Resource not found | +| `gqlutils.Forbidden(ctx, msg)` | Authorization failure | +| `gqlutils.Unauthenticatedf(ctx, msg, ...)` | Missing/invalid authentication | +| `gqlutils.Invalidf(ctx, msg, ...)` | Validation failure | +| `gqlutils.AssumptionRequired(ctx)` | Organization session not assumed | + +Always log unexpected errors before returning `gqlutils.Internal`: + +```go +// See: pkg/server/api/console/v1/v1_resolver.go +r.logger.ErrorCtx(ctx, "cannot load vendor", log.Error(err)) +return nil, gqlutils.Internal(ctx) +``` + +### MCP panic-based authorization (module-specific -- pkg/server/api/mcp/v1) + +MCP resolvers use `MustAuthorize()` which deliberately panics on authorization failure. A `RecoveryMiddleware` catches the panic and translates `iam.ErrInsufficientPermissions` to a permission-denied MCP error. This differs from GraphQL resolvers which return errors. + +```go +// See: pkg/server/api/mcp/v1/schema.resolvers.go +r.MustAuthorize(ctx, input.ID, probo.ActionVendorUpdate) +``` + +## Data Access + +### Scoper pattern for tenant isolation (universal -- pkg/coredata) + +Every query is tenant-scoped via the `Scoper` interface. Entity structs do NOT have a `TenantID` field -- `tenant_id` is injected at query time. + +Two implementations: +- `Scope` -- adds `tenant_id = @tenant_id` WHERE clause (normal queries) +- `NoScope` -- returns `TRUE` (cross-tenant admin queries) + +**Warning:** `NoScope.GetTenantID()` panics by design. Never call it. + +```go +// See: pkg/coredata/asset.go +q := fmt.Sprintf(q, scope.SQLFragment()) +args := pgx.StrictNamedArgs{"asset_id": assetID} +maps.Copy(args, scope.SQLArguments()) +``` + +### pgx.StrictNamedArgs (universal, enforced in code review) + +All queries must use `pgx.StrictNamedArgs`, not `pgx.NamedArgs`. StrictNamedArgs rejects any key present in the SQL that is not in the args map at runtime, preventing silent bugs from unset parameters. Using `pgx.NamedArgs` is an **approval blocker**. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). + +### Static SQL fragments (universal, enforced in code review) + +`SQLFragment()` must return a static SQL string -- no conditional string building. This ensures the prepared statement shape is always the same. Use `CASE WHEN` in SQL to handle optional filters. + +Conditional string building in `SQLFragment()` is an **approval blocker**. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). + +### Argument merging with maps.Copy (universal) + +Always use `maps.Copy` to combine args from scope, filter, and cursor. Never manually merge StrictNamedArgs maps. + +```go +// See: pkg/coredata/asset.go +args := pgx.StrictNamedArgs{"asset_id": assetID} +maps.Copy(args, scope.SQLArguments()) +maps.Copy(args, filter.SQLArguments()) +maps.Copy(args, cursor.SQLArguments()) +``` + +### Transaction handling (universal) + +- `pg.WithTx` for any operation involving multiple writes +- `pg.WithConn` for reads and single-step writes +- Webhook insertion always happens inside the same transaction as the mutating operation + +```go +// See: pkg/probo/vendor_service.go +return s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { + if err := vendor.Insert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert vendor: %w", err) + } + return webhook.InsertData(ctx, conn, s.svc.scope, webhooktypes.EventVendorCreated, vendor) +}) +``` + +### Cursor-based pagination (universal) + +All pagination uses keyset cursor pagination (not OFFSET). Cursors encode as `base64url(JSON)` with `[entity_global_id, sort_field_value]`. Default page size is 25. See `pkg/page` for the shared primitives. + +### No speculative indexes (universal, enforced in code review) + +No indexes added by default in migrations. Only add when justified by observed query latency. Unique/constraint indexes are exempt. This was explicitly called out in review (PR #917). + +## Authorization + +### ABAC policy system (universal) + +The IAM system uses an AWS-like evaluation model: **explicit deny > explicit allow > implicit deny**. Action strings follow the format `service:resource:operation` (e.g., `core:vendor:create`). + +Five built-in roles: OWNER, ADMIN, VIEWER, AUDITOR, EMPLOYEE. Policies are defined in `pkg/probo/policies.go` and `pkg/iam/iam_policies.go`. + +The authorization call flow: +1. Resolver calls `r.authorize(ctx, resourceID, actionString)` +2. `AuthorizeFunc` (from `pkg/server/api/authz`) extracts identity/session from context +3. Delegates to `iam.Authorizer.Authorize` +4. Authorizer resolves membership/role, evaluates policies, records audit log on Allow + +### Resource-state access control belongs in ABAC, not UI (universal, enforced in code review) + +When a resource's state (e.g., archived) affects allowed operations, enforcement must happen in the ABAC policy system (`pkg/probo/policies.go`), not in frontend visibility toggles. Prefer Allow rules on active states over Deny rules on archived states. This was debated in PR #855 and is now an established convention. + +### AuthorizationAttributer interface (universal -- pkg/coredata) + +Resources that need authorization must implement `AuthorizationAttributes(ctx, conn) (map[string]string, error)` in their coredata entity file. This returns attributes (typically `organization_id`) used by ABAC condition evaluation. + +## Dependency Injection + +### Constructor injection (universal) + +No DI framework. All dependencies are passed as explicit constructor parameters. The pattern varies by layer: + +- **Service layer**: `NewService(ctx, pgClient, s3Client, config, ...)` with a flat `Config` struct +- **Sub-modules**: sub-services receive `*TenantService` as their single field +- **Background workers**: functional options (`With*` functions) for optional tuning knobs +- **HTTP handlers**: `NewMux(logger, svc, config, ...)` returns `*chi.Mux` + +### Functional options for optional configuration (universal) + +```go +// See: pkg/iam/scim/bridge/bridge.go +type Option func(*Bridge) + +func WithDryRun(dryRun bool) Option { + return func(s *Bridge) { + s.dryRun = dryRun + } +} +``` + +Used across: `pkg/agent` (WithTools, WithLogger, WithModel), `pkg/llm` (WithHTTPClient, WithBaseURL), `pkg/iam/scim` (WithGarbageCollectionInterval), `pkg/probo` (WithEvidenceDescriptionWorkerInterval). + +## Observability + +### Structured logging (universal) + +Framework: `go.gearno.de/kit/log` -- context-aware, typed fields, named loggers. + +```go +// See: pkg/iam/saml/gc.go +l.InfoCtx(ctx, "garbage collection completed", + log.Int64("deleted_assertions", deletedAssertions), + log.Int64("deleted_requests", deletedRequests), + log.Duration("duration", time.Since(start)), +) +``` + +**Never log PII, PHI, or sensitive data** -- log opaque identifiers (GIDs, request IDs) only. This is a project-wide rule. See [shared.md -- Observability](../shared.md#observability). + +### OpenTelemetry tracing (universal) + +- GraphQL resolvers traced via `pkg/server/gqlutils/tracing.go` (TracingExtension) +- LLM calls traced with GenAI semantic conventions in `pkg/llm/llm.go` +- Agent runs traced in `pkg/agent/run.go` (span per run + per tool call) +- SCIM bridge runner traced in `pkg/iam/scim/bridge_runner.go` +- Traces exported to Grafana Tempo (OTLP on port 4317 in dev) + +### No logging in pkg/coredata (universal) + +The data access layer does not log. Errors are returned to callers who log at the service layer. + +## Background Workers + +### Poll-based worker pattern (universal) + +Workers use `time.Ticker` in a `for/select` loop with `FOR UPDATE SKIP LOCKED` to claim work items. Pattern documented in `contrib/claude/go-worker.md`. + +Key elements: +- Semaphore channel for bounded concurrency +- `context.WithoutCancel` for in-flight work that must complete after shutdown +- Stale-row recovery on each tick +- Drain loop until `ErrResourceNotFound` + +Workers using this pattern: `EvidenceDescriptionWorker` (pkg/probo), `BridgeRunner` (pkg/iam/scim), `Sender` (pkg/webhook), `SendingWorker` (pkg/mailer), `Provisioner` / `Renewer` (pkg/certmanager), `GarbageCollector` (pkg/iam/saml, pkg/iam/oidc). + +### Service.Run orchestration (universal) + +Top-level `Run(ctx) error` methods start child workers as goroutines. Uses `context.WithCancelCause` for crash propagation, `context.WithoutCancel` for in-flight work isolation, and ordered shutdown with `sync.WaitGroup`. + +See `contrib/claude/go-service.md` for the full pattern. diff --git a/.claude/guidelines/go-backend/pitfalls.md b/.claude/guidelines/go-backend/pitfalls.md new file mode 100644 index 0000000000..d218c99370 --- /dev/null +++ b/.claude/guidelines/go-backend/pitfalls.md @@ -0,0 +1,240 @@ +# Probo -- Go Backend -- Pitfalls + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +## Data Access Pitfalls + +### Using pgx.NamedArgs instead of pgx.StrictNamedArgs + +**What goes wrong:** Silent bugs from unset parameters. `pgx.NamedArgs` silently ignores keys present in SQL but missing from the args map. A filter parameter that should restrict results is quietly NULL, returning all rows. + +**Why it happens:** `pgx.NamedArgs` is the more commonly known type. Developers who are new to the project reach for it by default. + +**How to avoid:** Always use `pgx.StrictNamedArgs`. It rejects any key referenced in the SQL that is not present in the args map at runtime. Declare ALL keys in every code path -- use `nil` for inactive filters. + +**Source:** Code review approval blocker (PR #845 -- "CLAUDE.md violation: pgx.NamedArgs instead of pgx.StrictNamedArgs"). See `pkg/coredata/finding_filter.go` for the correct pattern. + +### Conditional string building in SQLFragment() + +**What goes wrong:** The prepared statement plan changes shape depending on runtime conditions, defeating PostgreSQL's plan cache and potentially causing subtle query-plan bugs. + +**Why it happens:** It feels natural to build SQL strings with Go `if` blocks, appending AND clauses conditionally. + +**How to avoid:** `SQLFragment()` must return a static SQL string. Use `CASE WHEN` in the SQL itself to handle optional filters. All referenced args keys must always be declared (use `nil` for inactive ones). + +**Source:** Code review approval blocker (PR #845 -- "CLAUDE.md violation: SQLFragment() uses conditional string building"). See `pkg/coredata/vendor_filter.go` for the correct pattern. + +### CTE queries missing tenant_id qualification + +**What goes wrong:** When a CTE joins tables that both have `tenant_id`, the outer query's `scope.SQLFragment()` produces an unqualified `tenant_id = @tenant_id` reference, causing a runtime "ambiguous column" SQL error. + +**Why it happens:** The scope fragment is injected via `%s` and always produces `tenant_id = @tenant_id` without table qualification. This works fine for single-table queries but breaks multi-table CTEs. + +**How to avoid:** Always include the qualified `tenant_id` column (e.g., `fi.tenant_id`) in the CTE's SELECT list so the outer scope reference is unambiguous. + +**Source:** Code review bug catch (PR #845 -- "Bug: CTE missing tenant_id -- will cause runtime SQL error"). + +### Storing tenant_id on entity structs + +**What goes wrong:** Violates the Scoper invariant. The coredata convention is that entity structs have no TenantID field -- tenant isolation is enforced via the Scoper at query time. Storing it on the struct creates a source of truth conflict. + +**Why it happens:** It seems convenient to have the tenant ID on the struct for logging or passing around. One legacy entity (Vendor) has a TenantID field, which can mislead by example. + +**How to avoid:** Never add a TenantID field to new entity structs. Use `scope.GetTenantID()` at query time. See `pkg/coredata/scope.go`. + +**Source:** Documented in `pkg/coredata/CLAUDE.md`. + +### Calling NoScope.GetTenantID() + +**What goes wrong:** Panics immediately. `NoScope` is only for cross-tenant admin queries; it has no tenant ID. + +**Why it happens:** Developers use `NoScope` for admin operations that also need to insert records (which requires `GetTenantID()`). + +**How to avoid:** Only use `NoScope` for read-only cross-tenant queries. For inserts, always use a `Scope` with a specific tenant. + +**Source:** `pkg/coredata/scope.go` line 56. + +### Adding speculative database indexes + +**What goes wrong:** Unnecessary indexes slow down writes and increase storage. The team explicitly rejects premature indexes. + +**Why it happens:** Coming from other projects where indexing is standard practice for new tables. + +**How to avoid:** No indexes by default. Only add when justified by observed query latency in production. Unique/constraint indexes are exempt. + +**Source:** Code review (PR #917 -- "No index by default. Only if we have performance issue."). + +### Using subqueries in SELECT instead of JOINs + +**What goes wrong:** Harder to read, and the query planner may not optimize them as expected. + +**Why it happens:** Subqueries can feel more natural when fetching a single related value. + +**How to avoid:** Prefer JOINs or CTEs for readability. Restructure complex queries to avoid SELECT-clause subqueries. + +**Source:** Code review (PR #917 -- "Sub query in the select seams like a smell"). + +## Resolver Pitfalls + +### Using panic in GraphQL resolvers + +**What goes wrong:** Unrecovered panics crash the server. Even recovered panics produce opaque 500 errors instead of proper GraphQL error responses. + +**Why it happens:** Quick error handling shortcut, especially when copying patterns from MCP resolvers (which deliberately use MustAuthorize + panic). + +**How to avoid:** Always return errors in GraphQL resolvers. A dedicated cleanup PR (#865, 1425 lines) was merged to eliminate all instances. Exception: MCP resolvers use `MustAuthorize()` which deliberately panics, caught by `RecoveryMiddleware`. + +**Source:** Code review approval blocker (PR #865). + +### Missing authorization before service calls in resolvers + +**What goes wrong:** Data is returned or modified without access control. The resolver pattern requires authorize as the very first step. + +**Why it happens:** Easy to forget when adding a new resolver, especially for read operations that feel harmless. + +**How to avoid:** Every resolver method must call `r.authorize(ctx, resourceID, action)` as step 1. The sequence is: (1) Authorize, (2) Get service, (3) Call service, (4) Handle error. + +**Source:** Documented in `pkg/server/api/console/v1/CLAUDE.md`. + +### Using direct service calls for entities that have dataloaders + +**What goes wrong:** N+1 queries. If a resolver fetches an entity that has a dataloader via `prb.Entity.Get()` instead of `loaders.Entity.Load()`, each GraphQL field resolution triggers a separate database query. + +**Why it happens:** Direct service calls are more intuitive and work correctly -- they just perform badly. + +**How to avoid:** Check `pkg/server/api/console/v1/dataloader/dataloader.go` for the 11 entity types with batch loaders (Organization, Framework, Control, Vendor, Document, Profile, Risk, Measure, Task, File, Report). Always use the dataloader for these. + +**Source:** Codebase pattern in `pkg/server/api/console/v1/dataloader/`. + +### Editing generated files + +**What goes wrong:** All manual edits are silently overwritten next time `go generate` runs. + +**Why it happens:** The generated files (`schema/schema.go`, `types/types.go`, `server/server.go`) look like regular code and are easy to edit accidentally. + +**How to avoid:** Never edit files in `schema/` or `types/types.go`. After changing `schema.graphql`, run `go generate ./pkg/server/api/console/v1`. For MCP, edit `specification.yaml` then run `go generate ./pkg/server/api/mcp/v1`. + +**Source:** Documented in `CLAUDE.md` and all API package `CLAUDE.md` files. + +### Missing node resolver for types implementing Node + +**What goes wrong:** The GraphQL schema declares `implements Node` but the resolver has no corresponding implementation. The API surface is incomplete -- clients cannot refetch the node by ID. + +**Why it happens:** Adding the `implements Node` declaration to the schema is easy; implementing the resolver is a separate step that can be forgotten. + +**How to avoid:** Every type that implements Node must have a resolver in the `node()` query path. + +**Source:** Code review (PR #909 -- "It implements node but there is no node on the resolver."). + +## Entity Registration Pitfalls + +### Reusing a removed entity type number + +**What goes wrong:** GIDs stored in the database with the old type number will be misinterpreted as the new type, corrupting entity resolution. + +**Why it happens:** The gap in the entity type registry (`entity_type_reg.go`) looks like it should be filled. + +**How to avoid:** Removed types are tombstoned as `_ uint16 = N // EntityType - removed`. Always append new types at the end. Never fill gaps. + +**Source:** `pkg/coredata/entity_type_reg.go` lines 34, 54, 58, 70. + +### Forgetting to register a new entity in NewEntityFromID + +**What goes wrong:** The function silently returns `(nil, false)` for unregistered types. Authorization and node resolution break silently. + +**Why it happens:** Adding the const is one step; adding the switch case is a separate step in the same file. + +**How to avoid:** When adding a new entity type constant, always add the corresponding case in the `NewEntityFromID` switch in the same file. + +**Source:** `pkg/coredata/entity_type_reg.go`. + +## Authentication Middleware Pitfalls + +### Wrong middleware order + +**What goes wrong:** Authentication checks fail silently or mutual exclusion checks miss. + +**Why it happens:** The three middlewares (session, API key, identity presence) depend on a specific ordering. + +**How to avoid:** The canonical order is: `NewSessionMiddleware` -> `NewAPIKeyMiddleware` -> `NewIdentityPresenceMiddleware`. Identity presence must always be last -- it only checks context, it does not perform authentication. + +**Source:** `pkg/server/api/console/v1/resolver.go` lines 89-91. + +## Service Layer Pitfalls + +### Over-engineered service logic + +**What goes wrong:** Code review rejects the PR for unnecessary complexity. + +**Why it happens:** Developers add defensive patterns (e.g., "if updated" checks) that don't match the project's straightforward style. + +**How to avoid:** Keep service methods simple and direct. If reviewers say "useless complex logic," simplify. The team values clarity over defensive programming. + +**Source:** Code review (PR #898 -- "useless complex logic" and "We don't use this 'if updated' pattern in any other updates."). + +## Validation Pitfalls + +### Forgetting Required() on mandatory fields + +**What goes wrong:** All validators except `Required()` silently pass nil values. A mandatory field with only `SafeText()` will pass validation when nil. + +**Why it happens:** The design philosophy is "nil = optional." This is the opposite of most validation libraries. + +**How to avoid:** Always include `Required()` in the validator chain for mandatory fields. For optional fields, the nil-passing behavior is intentional. + +**Source:** Documented in `contrib/claude/validation.md` and tested in `pkg/validator/validation_test.go`. + +### Using google/uuid instead of gearno.de/crypto/uuid + +**What goes wrong:** Build may work but code review will reject it. The project explicitly bans `github.com/google/uuid`. + +**Why it happens:** `google/uuid` is the most common Go UUID library and appears first in search results. + +**How to avoid:** Always use `go.gearno.de/crypto/uuid`. For entity IDs, use `gid.New(tenantID, entityType)` instead of UUIDs. + +**Source:** Documented in `CLAUDE.md`. + +## Filter Design Pitfalls + +### Using boolean flags instead of State[] slices for multi-state filtering + +**What goes wrong:** Combinatorial flag proliferation. Adding a third state requires exponential flag combinations. + +**Why it happens:** Boolean flags seem simpler for two-state filtering. + +**How to avoid:** Use a `State[]` slice filter approach for multi-state queries. This allows flexible multi-state queries without combinatorial growth. + +**Source:** Code review (PR #855 -- "let refactor to have State[] and filter by state instead of that"). + +### Using virtual/computed columns instead of real columns + +**What goes wrong:** Virtual columns computed in SELECT are harder to maintain and don't participate in indexes. + +**Why it happens:** Feels efficient to derive a value in the query rather than adding a column. + +**How to avoid:** Prefer adding a real database column to the schema rather than computing a derived value in SELECT. + +**Source:** Code review (PR #855 -- "Let put a real column here, instead of virtual one like that"). + +## GraphQL Schema Pitfalls + +### Missing default filter on Organization query fields + +**What goes wrong:** The query returns an expensive unfiltered result set. + +**Why it happens:** Fields on Organization that accept a filter argument are added without specifying a default. + +**How to avoid:** Always provide a default filter value in the schema: `filter: SomeFilter = { snapshotId: null }`. + +**Source:** Code review (PR #845 -- "Bug: Missing default snapshot filter on 'findings' field"). + +### Audit log FK missing ON DELETE CASCADE + +**What goes wrong:** Deleting an organization fails because audit log rows still reference it. + +**Why it happens:** Default FK behavior is RESTRICT. + +**How to avoid:** Audit log tables should use `ON DELETE CASCADE` for the parent organization FK so deletion cascades automatically. + +**Source:** Code review (PR #909 -- "We want if we delete the org we don't need keep those logs."). diff --git a/.claude/guidelines/go-backend/testing.md b/.claude/guidelines/go-backend/testing.md new file mode 100644 index 0000000000..2ea2295f08 --- /dev/null +++ b/.claude/guidelines/go-backend/testing.md @@ -0,0 +1,319 @@ +# Probo -- Go Backend -- Testing + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +## Framework & Tooling + +- **Test runner**: `gotestsum` wrapping `go test` (used in CI) +- **Assertions**: `github.com/stretchr/testify` (`require` for fatal, `assert` for non-fatal) +- **Race detection**: `-race` flag always enabled (`make test` includes it) +- **Coverage**: `-cover -coverprofile=coverage.out` (CI generates HTML reports) +- **Build command**: `make test` (all tests) or `make test MODULE=./pkg/foo` (single module) +- **E2E command**: `make test-e2e` (requires `bin/probod` built first) +- **Verbose**: `make test-verbose` + +## Test Organization & File Naming + +### Package naming (universal) + +Black-box test packages (`package foo_test`) are preferred. White-box packages (`package foo`) are used only when testing unexported functions. + +```go +// Black-box (preferred) -- See: pkg/iam/policy/example_test.go +package policy_test + +// White-box (only for unexported) -- See: pkg/iam/policy/evaluator_test.go +package policy +``` + +### Test naming (universal) + +Top-level: `TestFunctionName_Scenario` or `TestEntity_Operation`. +Subtests: `t.Run` with lowercase descriptive names. + +```go +// See: e2e/console/vendor_test.go +func TestVendor_Create(t *testing.T) { + t.Parallel() + + t.Run("with full details", func(t *testing.T) { + t.Parallel() + // ... + }) + + t.Run("viewer cannot create vendor", func(t *testing.T) { + t.Parallel() + // ... + }) +} +``` + +### Parallel execution (universal, enforced in code review) + +**Always call `t.Parallel()` at both the top-level test function and every subtest.** Missing `t.Parallel()` calls in subtests are flagged as CLAUDE.md violations and are an **approval blocker**. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). + +### Test file location (universal) + +- **Unit tests**: co-located as `_test.go` in the same package +- **E2E tests**: one file per entity in `e2e/console/` (package `console_test`) + +### Mock definitions (universal) + +Define mock types and helpers at the top of the test file, not inline. Mocks implement the interface under test. + +```go +// See: pkg/llm/llm_test.go +type mockProvider struct { + chatCompletionFunc func(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) +} + +func (m *mockProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) { + return m.chatCompletionFunc(ctx, req) +} +``` + +## require vs assert + +This distinction is critical and enforced across the codebase: + +| Function | Library | Use when | Behavior | +|----------|---------|----------|----------| +| `require.*` | `testify/require` | Preconditions that must succeed | Stops the test immediately on failure | +| `assert.*` | `testify/assert` | Value verification | Continues test after failure | + +```go +// See: e2e/console/vendor_test.go +// require for preconditions -- test cannot continue without these +result, err := c.Execute(t, query, variables) +require.NoError(t, err) + +// assert for value checks -- continue to check more fields +assert.Equal(t, name, result.Data.CreateVendor.Vendor.Name) +assert.Equal(t, description, result.Data.CreateVendor.Vendor.Description) +``` + +Common require functions: `NoError`, `Error`, `ErrorAs`, `Len`, `NotNil`. +Common assert functions: `Equal`, `Contains`, `True`, `False`, `Nil`. + +## End-to-End Testing + +### Architecture + +E2E tests run against a live `bin/probod` subprocess with a real PostgreSQL database. `testutil.Setup()` starts the server once per test run via `sync.Once`. Each test creates isolated organizations and users, so tests never share state. + +### Test structure + +Every e2e test follows this pattern: + +```go +// See: e2e/console/vendor_test.go +func TestVendor_Create(t *testing.T) { + t.Parallel() + + // 1. Create test client (isolated org + user) + owner := testutil.NewClient(t, testutil.RoleOwner) + + // 2. Create test data via factory + name := factory.SafeName("vendor") + + // 3. Execute GraphQL mutation + result, err := owner.Execute(t, createVendorQuery, map[string]any{ + "input": map[string]any{"name": name}, + }) + require.NoError(t, err) + + // 4. Assert results + assert.Equal(t, name, result.Data.CreateVendor.Vendor.Name) +} +``` + +### Test data factories + +Two patterns for creating test data: + +```go +// Simple function -- See: e2e/internal/factory/factory.go +vendor := factory.CreateVendor(t, client) + +// Fluent builder -- See: e2e/internal/factory/factory.go +vendor := factory.NewVendor(client). + WithName("Custom Name"). + WithDescription("Custom description"). + Create(t) +``` + +Use `factory.SafeName(prefix)` for unique names and `factory.SafeEmail()` for unique emails. These use gofakeit to prevent collisions in parallel tests. + +### RBAC testing (universal -- required for every entity) + +Every entity must test role-based access control: + +```go +// See: e2e/console/vendor_test.go +t.Run("viewer cannot create vendor", func(t *testing.T) { + t.Parallel() + viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + _, err := viewer.Execute(t, createVendorQuery, variables) + testutil.RequireForbiddenError(t, err) +}) +``` + +Test all role combinations: Owner, Admin, Viewer for create/update/delete/read. + +### Tenant isolation testing (universal -- required for every entity) + +Every entity must verify that cross-organization access is denied: + +```go +// See: e2e/console/vendor_test.go +t.Run("cannot read vendor from another organization", func(t *testing.T) { + t.Parallel() + otherOwner := testutil.NewClient(t, testutil.RoleOwner) + testutil.AssertNodeNotAccessible(t, otherOwner, vendor.ID) +}) +``` + +### Assertion helpers + +| Helper | Purpose | +|--------|---------| +| `testutil.RequireForbiddenError(t, err)` | Verify RBAC denial | +| `testutil.RequireErrorCode(t, err, code)` | Check specific GraphQL error code | +| `testutil.AssertTimestampsOnCreate(t, created, updated, before)` | Verify createdAt == updatedAt on create | +| `testutil.AssertTimestampsOnUpdate(t, created, updated, origCreated, origUpdated)` | Verify updatedAt advances on update | +| `testutil.AssertFirstPage(t, pageInfo)` | Verify first page of pagination | +| `testutil.AssertLastPage(t, pageInfo)` | Verify last page of pagination | +| `testutil.AssertNodeNotAccessible(t, client, id)` | Verify tenant isolation | +| `testutil.AssertOrderedAscending(t, items)` | Verify sort order | + +### Inline GraphQL queries + +E2E tests define GraphQL queries as package-level `const` strings with typed result structs: + +```go +// See: e2e/console/vendor_test.go +const createVendorQuery = ` + mutation CreateVendor($input: CreateVendorInput!) { + createVendor(input: $input) { + vendor { + id + name + description + createdAt + updatedAt + } + } + } +` + +type createVendorResult struct { + Data struct { + CreateVendor struct { + Vendor struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"vendor"` + } `json:"createVendor"` + } `json:"data"` +} +``` + +### Validation scenario testing + +Table-driven tests for validation (HTML injection, control chars, max length): + +```go +// See pattern from contrib/claude/e2e.md +tests := []struct{ + name string + input string + code string +}{ + {"html injection", "", "UNSAFE_CONTENT"}, + {"control characters", "name\x00with\x01control", "INVALID_FORMAT"}, + {"exceeds max length", strings.Repeat("a", 101), "TOO_LONG"}, +} + +for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // ... + testutil.RequireErrorCode(t, err, tt.code) + }) +} +``` + +## Unit Testing Patterns + +### Validator package (module-specific -- uses stdlib testing) + +The `pkg/validator` package uses stdlib `testing` without testify. All other packages use testify. + +```go +// See: pkg/validator/validation_test.go +func TestOptionalByDefault(t *testing.T) { + v := New() + v.Check((*string)(nil), "field", MinLen(1)) + if v.Error() != nil { + t.Errorf("expected nil pointer to pass non-Required validators") + } +} +``` + +### Policy package (module-specific -- uses Go example tests) + +`pkg/iam/policy` uses Go example tests with `Output:` assertions for documentation-as-tests: + +```go +// See: pkg/iam/policy/example_test.go +func Example_basicAuthorization() { + p := policy.NewPolicy("test", + policy.Allow("service:resource:read"), + ) + // ... + fmt.Println(result.Decision) + // Output: allow +} +``` + +### LLM/Agent tests (module-specific -- mock providers) + +Tests for `pkg/llm` and `pkg/agent` use mock implementations of the Provider and ChatCompletionStream interfaces: + +```go +// See: pkg/llm/llm_test.go +func newTestClient(provider llm.Provider) (*llm.Client, *tracetest.SpanRecorder) { + recorder := tracetest.NewSpanRecorder() + tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) + client := llm.NewClient("test-system", provider, + llm.WithTracerProvider(tp), + ) + return client, recorder +} +``` + +## Coverage Expectations + +No explicit coverage threshold is enforced. The CI pipeline generates coverage reports (`coverage.out`, HTML). Several key packages have no unit tests at all (pkg/coredata, pkg/trust, pkg/iam/oidc, pkg/server/api/authn) -- their coverage comes exclusively from e2e tests. + +The project philosophy is that e2e tests against the live server provide the most valuable coverage for the API layer, while unit tests focus on pure logic packages (validator, policy, llm, agent). + +## New Entity E2E Checklist + +From `contrib/claude/e2e.md`, every new entity must have tests covering: + +1. Create with full details +2. Create with minimal details +3. Update all fields +4. Update single field +5. Delete +6. List with pagination (first page, next page, ordering) +7. RBAC: owner/admin can create, viewer cannot +8. RBAC: owner/admin can update, viewer cannot +9. RBAC: owner/admin can delete, viewer cannot +10. Tenant isolation: cross-org user cannot access resource +11. Timestamps: createdAt == updatedAt on create, updatedAt advances on update From 142077f235451ffbf66ef3a34883cbdc98117e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:49:50 +0200 Subject: [PATCH 3/9] Add TypeScript frontend guidelines for AI agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .../typescript-frontend/conventions.md | 176 +++++++++++ .../guidelines/typescript-frontend/index.md | 64 ++++ .../module-notes/apps-console.md | 78 +++++ .../module-notes/apps-trust.md | 73 +++++ .../module-notes/packages-emails.md | 90 ++++++ .../module-notes/packages-n8n-node.md | 69 +++++ .../typescript-frontend/patterns.md | 284 ++++++++++++++++++ .../typescript-frontend/pitfalls.md | 160 ++++++++++ .../guidelines/typescript-frontend/testing.md | 158 ++++++++++ 9 files changed, 1152 insertions(+) create mode 100644 .claude/guidelines/typescript-frontend/conventions.md create mode 100644 .claude/guidelines/typescript-frontend/index.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/apps-console.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/apps-trust.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-emails.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-n8n-node.md create mode 100644 .claude/guidelines/typescript-frontend/patterns.md create mode 100644 .claude/guidelines/typescript-frontend/pitfalls.md create mode 100644 .claude/guidelines/typescript-frontend/testing.md diff --git a/.claude/guidelines/typescript-frontend/conventions.md b/.claude/guidelines/typescript-frontend/conventions.md new file mode 100644 index 0000000000..5564fdd066 --- /dev/null +++ b/.claude/guidelines/typescript-frontend/conventions.md @@ -0,0 +1,176 @@ +# Probo -- TypeScript Frontend -- Conventions + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +See [shared.md -- Git & Workflow](../shared.md#git--workflow) for commit format, branching, and merge strategy. +See [shared.md -- License](../shared.md#license) for ISC license header requirements on all source files. + +## TypeScript Configuration + +- **Strict mode**: enabled across apps and packages via shared `@probo/tsconfig` +- **Target**: ES modules (`"type": "module"` in all `package.json` files) +- **Source-level consumption**: shared packages point `"main"` directly at `./src/index.ts` (raw TypeScript). This works with Vite but will not work in plain Node.js environments. + +## Code Style + +ESLint configuration is centralized in `@probo/eslint-config` (`packages/eslint-config/`). Key rules enforced: + +### Formatting (via `@stylistic/eslint-plugin`) + +| Rule | Setting | +|------|---------| +| Indent | 2 spaces | +| Quotes | Double quotes (`"`) | +| Semicolons | Always required | +| Trailing commas | `always-multiline` | +| Arrow parens | As-needed | +| Brace style | `1tbs` | +| Max line length | 120 characters (warn), ignores strings/templates/comments/URLs | + +### Import Ordering (via `eslint-plugin-import-x`) + +Three groups separated by blank lines: + +1. **Built-in / External** packages (e.g., `react`, `react-relay`, `@probo/ui`) +2. **Aliased imports** using the `#/` prefix (e.g., `#/components/`, `#/hooks/`) +3. **Relative imports** (parent, sibling, index) + +Within each group, imports are sorted alphabetically (case-insensitive). The `#/` alias resolves to `src/` within each app. + +```tsx +// Example from apps/trust/src/pages/DocumentPage.tsx +import { formatError } from "@probo/helpers"; +import { useSystemTheme } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; +import { UnAuthenticatedError } from "@probo/relay"; +import { Button, IconArrowDown, Spinner, useToast } from "@probo/ui"; +import { useEffect, useState } from "react"; +import { useMutation, usePreloadedQuery } from "react-relay"; +import { Link, useNavigate } from "react-router"; +import { graphql } from "relay-runtime"; + +import { PDFPreview } from "#/components/PDFPreview"; +import { getPathPrefix } from "#/utils/pathPrefix"; + +import type { DocumentPageQuery } from "./__generated__/DocumentPageQuery.graphql"; +``` + +### Relay ESLint Rules (via `eslint-plugin-relay`) + +The `ts-recommended` ruleset is enabled. Key rules: +- `relay/must-colocate-fragment-spreads` -- fragments must be in the same file as their spread +- `relay/unused-fields` -- all fetched fields must be used + +Legacy `hooks/graph/` files carry `eslint-disable` comments for these rules. New code must not add such comments. + +## Naming Conventions + +### File Naming (universal) + +| Type | Convention | Example | +|------|-----------|---------| +| React components, pages, layouts | PascalCase `.tsx` | `RisksPage.tsx`, `Badge.tsx`, `MainLayout.tsx` | +| Hooks | camelCase with `use` prefix | `useToggle.ts`, `useMutationWithToasts.ts` | +| Route files | camelCase with `Routes` suffix | `riskRoutes.ts`, `assetRoutes.ts` | +| Loader components | PascalCase with `Loader` suffix | `DocumentsPageLoader.tsx`, `MeasuresPageLoader.tsx` | +| Helper/utility files | camelCase | `pathPrefix.ts`, `audits.ts`, `error.ts` | +| Test files | `.test.ts` co-located | `file.test.ts`, `array.test.ts` | +| Story files | `.stories.tsx` co-located | `Button.stories.tsx` | +| Constants | `constants.ts` | `constants.ts` | + +### Component and Variable Naming (universal) + +- **Components**: PascalCase function names matching the file name +- **Hooks**: `use` prefix, camelCase (`useToggle`, `useFormWithSchema`, `useOrganizationId`) +- **Relay fragments**: name matches `{ComponentName}Fragment_{fieldName}` (e.g., `VendorContactsTabFragment_contact`) +- **Relay queries**: name matches `{ComponentName}Query` (e.g., `DocumentsPageQuery`) +- **Relay mutations**: name matches `{ComponentName}{Action}Mutation` (e.g., `DocumentPageExportDocumentMutation`) +- **Route arrays**: camelCase with `Routes` suffix (e.g., `const assetRoutes = [...]`) +- **Relay environments**: camelCase descriptive name (`coreEnvironment`, `iamEnvironment`, `consoleEnvironment`) + +### Domain Helper Naming (module-specific: packages/helpers) + +Each domain file follows a strict naming pattern: + +- Enum array: `const fooStates = [...] as const` (or `fooTypes`, `fooCategories`) +- Label function: `getFooStateLabel(__: Translator, state)` -- translator is always the first argument +- Variant function: `getFooStateVariant(state)` -- returns badge variant string +- Options function: `getFooStateOptions(__)` -- returns `{ value, label }[]` for dropdowns + +See `packages/helpers/src/audits.ts` for the canonical example. + +## Export Patterns (universal) + +- **Named exports** everywhere. Default exports are used only for page components (required by `React.lazy`). +- **Barrel files**: `src/index.ts` re-exports all public symbols from each package. +- All public exports from `@probo/ui` go through `packages/ui/src/index.ts`. +- Icon components have their own barrel at `packages/ui/src/Atoms/Icons/index.tsx`. + +## Directory Structure + +### Apps (apps/console, apps/trust) + +``` +src/ + __generated__/ # Relay compiler output (never edit) + components/ # Shared cross-domain UI components + hooks/ # Reusable hooks + graph/ # LEGACY -- Relay queries/fragments (do not add new files) + forms/ # react-hook-form + zod schema hooks + pages/ # Route-level page components + organizations/ # Business domain pages (console) + / # One directory per domain + _components/ # Private sub-components + dialogs/ # Modal dialogs with mutation handling + tabs/ # Detail-page tab components + providers/ # React context providers + routes/ # Route definition files (console only) + utils/ # Utility functions + environments.ts # Relay environment configuration (console) + routes.tsx # Route assembly + main.tsx # App entry point + types.ts # Shared utility types (NodeOf, ItemOf) +``` + +### Shared Packages + +| Package | Structure | +|---------|-----------| +| `packages/ui` | `src/Atoms//`, `src/Molecules//`, `src/Layouts/` | +| `packages/helpers` | Flat `src/` -- one file per domain, all re-exported from `index.ts` | +| `packages/hooks` | Flat `src/` -- one file per hook, barrel `index.ts` | +| `packages/relay` | Flat `src/` -- `errors.ts`, `fetch.ts`, `index.ts` | +| `packages/emails` | `src/` (TSX templates), `templates/` (.txt), `scripts/` (build), `assets/` (images) | + +### Relay Generated Files + +Generated types land in `__generated__/` subdirectories relative to the file declaring the operation: + +- Console core API: `apps/console/src/__generated__/core/` +- Console IAM API: `apps/console/src/__generated__/iam/` +- Trust API: `apps/trust/src/**/__generated__/` (next to declaring files) + +These files must never be edited by hand. Regenerate with `npm run relay` or `npx relay-compiler`. + +## i18n Conventions (universal) + +All user-visible strings in components and molecules go through the `useTranslate()` hook from `@probo/i18n`: + +```tsx +const { __ } = useTranslate(); +return ; +``` + +Domain helper functions accept a `Translator` (`(s: string) => string`) as their first argument rather than importing `useTranslate` directly, keeping the helpers decoupled from React. + +Note: the `TranslatorProvider` currently returns an empty translation map (stub). All `__()` calls fall back to the key string. This is intentional for now -- do not add new i18n infrastructure without addressing this TODO. + +## Review-Enforced Standards + +These patterns are enforced by code reviewers on TypeScript frontend PRs: + +- **Do not use `withQueryRef`** -- use a Loader component pattern instead. This is an approval blocker. (enforced in code review; see [shared.md -- Approval blockers](../shared.md#review-enforced-standards)) +- **Do not use `useMutationWithToasts`** -- use `useMutation` + `useToast` separately. (enforced in code review) +- **Access control in ABAC, not UI conditionals** -- when resource state affects allowed operations, enforcement must be in the IAM policy system, not in frontend visibility toggles. (enforced in code review; see [shared.md -- Review-Enforced Standards](../shared.md#review-enforced-standards)) +- **ISC license headers must be current** -- see [shared.md -- License](../shared.md#license). diff --git a/.claude/guidelines/typescript-frontend/index.md b/.claude/guidelines/typescript-frontend/index.md new file mode 100644 index 0000000000..9e911f3d83 --- /dev/null +++ b/.claude/guidelines/typescript-frontend/index.md @@ -0,0 +1,64 @@ +# Probo -- TypeScript Frontend + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) +> Sections wrapped in will be preserved during refresh. + +## Architecture Overview + +The TypeScript frontend is a monorepo workspace containing two React 19 SPAs and a collection of shared packages. The admin console (`apps/console`) and the public trust center (`apps/trust`) both use Relay 19 as their GraphQL client, React Router v7 for client-side routing with data loaders, and Tailwind CSS v4 for styling. They communicate with the Go backend through GraphQL schemas that serve as the cross-stack contract (see [shared.md -- Cross-Stack Contracts](../shared.md#cross-stack-contracts) for details). + +Both apps follow a feature-slice architecture where route files define the page tree, pages contain colocated Relay queries and fragments, and shared UI primitives live in `@probo/ui`. The Relay compiler reads the Go-authored `.graphql` schema files directly and generates TypeScript types into `__generated__/` directories -- there are no hand-written API types on the frontend. The compiled frontend assets are embedded into the Go binary (`bin/probod`), so `make build` must compile both apps before the Go build step. + +A set of shared packages (`packages/*`) provides the design system (`@probo/ui`), the Relay network layer (`@probo/relay`), domain helpers (`@probo/helpers`), shared hooks (`@probo/hooks`), routing utilities (`@probo/routes`), i18n (`@probo/i18n`), and ESLint configuration (`@probo/eslint-config`). Two additional packages serve specialized roles: `packages/emails` provides React Email templates compiled to Go template HTML, and `packages/n8n-node` is an n8n community node for the Probo API. + +## Module Map + +| Module | Package Name | Purpose | Key Patterns | +|--------|-------------|---------|--------------| +| `apps/console` | `@probo/console` | Admin dashboard SPA (port 5173) | Two Relay environments (core + IAM), feature-slice routes, snapshot mode, permission fragments, Loader component pattern | +| `apps/trust` | `@probo/trust` | Public trust center SPA (port 5174) | Single Relay environment, path-prefix routing, auth redirects with continue URLs, NDA signing flow | +| `packages/ui` | `@probo/ui` | Shared design system | Tailwind Variants (`tv()`), Radix primitives, Atom/Molecule/Layout hierarchy, Storybook 10 | +| `packages/relay` | `@probo/relay` | Relay FetchFunction factory + typed error classes | `makeFetchQuery`, typed GraphQL error-to-exception mapping | +| `packages/helpers` | `@probo/helpers` | Domain formatters, enum labels/variants, utilities | Translator injection pattern, `as const` enum arrays, `getXLabel`/`getXVariant` | +| `packages/hooks` | `@probo/hooks` | Shared React hooks | `useToggle`, `useList`, `usePageTitle`, `useCopy`, `useSystemTheme` | +| `packages/emails` | `@probo/emails` | React Email templates for transactional emails | TSX with Go template placeholders, build to `.html.tmpl`, embedded in Go binary | +| `packages/n8n-node` | `@probo/n8n-nodes-probo` | n8n community node for Probo API | Resource/Operation slices, inline GraphQL, cursor pagination | + +## Canonical Examples + +| File | What it demonstrates | +|------|---------------------| +| `apps/console/src/routes/documentsRoutes.ts` | New-style route definitions using Loader components (no `withQueryRef`), nested tab routes, lazy loading | +| `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` | Canonical Loader component: `useQueryLoader` + `useEffect` + Suspense + CoreRelayProvider | +| `apps/trust/src/pages/DocumentPage.tsx` | Full page with colocated queries/mutations, typed error handling, `__typename` dispatch, `getPathPrefix()` usage | +| `packages/ui/src/Atoms/Badge/Badge.tsx` | Canonical UI atom: `tv()` variant factory, `asChild`/`Slot`, typed props, named export | +| `packages/helpers/src/audits.ts` | Standard domain helper: `as const` enum array, `getXLabel` with Translator, `getXVariant` for badge variants | + +## Topic Index + +- [Patterns](./patterns.md) -- Relay data fetching, routing, component architecture, forms, error handling +- [Conventions](./conventions.md) -- TypeScript style, imports, naming, project structure +- [Testing](./testing.md) -- Storybook, vitest, e2e testing approach +- [Pitfalls](./pitfalls.md) -- common mistakes, deprecated patterns, things that break silently +- Module-specific notes: + - [apps/console](./module-notes/apps-console.md) -- dual Relay environments, snapshot mode, permission checks + - [apps/trust](./module-notes/apps-trust.md) -- path-prefix routing, auth flows, NDA signing + - [packages/emails](./module-notes/packages-emails.md) -- React Email + Go template hybrid + - [packages/n8n-node](./module-notes/packages-n8n-node.md) -- n8n community node conventions + + +## Team Notes + +_Reserved for manual additions by the team -- this section is preserved on refresh._ + + + +## Open Questions + +1. The `hooks/graph/` directory in `apps/console` contains substantial legacy code with eslint-disable comments. Is there a migration plan to inline these into consuming components? +2. `TranslatorProvider` in both apps is a stub (TODO in source). Is i18n support planned, and what loader mechanism will be used? +3. Some layout loaders use `useQueryLoader` + `useEffect` instead of the router-loader preload pattern. Is this intentional for nested layouts that cannot use React Router loaders directly? +4. `@tanstack/react-query` is mounted at the root of both apps but used minimally. Can it be removed from `apps/trust`? +5. Should unrecognised GraphQL error codes in `@probo/relay` throw a generic error rather than being silently passed through? + diff --git a/.claude/guidelines/typescript-frontend/module-notes/apps-console.md b/.claude/guidelines/typescript-frontend/module-notes/apps-console.md new file mode 100644 index 0000000000..a3b18c718c --- /dev/null +++ b/.claude/guidelines/typescript-frontend/module-notes/apps-console.md @@ -0,0 +1,78 @@ +# Probo -- TypeScript Frontend -- apps/console + +> Module-specific notes for `apps/console` (`@probo/console`) +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md) + +## Purpose + +The primary admin dashboard for compliance managers. A React 19 + Vite SPA (port 5173) that communicates with two GraphQL endpoints over Relay. It covers all compliance domains: risks, measures, documents, vendors, frameworks, audits, assets, data, tasks, processing activities, rights requests, snapshots, obligations, findings, and a public compliance/trust-center page manager. + +## Dual Relay Environments + +This is the only app with two Relay environments. See [patterns.md -- Relay Environments](../patterns.md#relay-environments-module-specific-appsconsole) for configuration details. + +**Critical rule**: IAM pages (`src/pages/iam/`) must use `IAMRelayProvider`. Organization pages (`src/pages/organizations/`) must use `CoreRelayProvider`. The Relay compiler config enforces this mapping at build time, but a runtime mismatch will cause silent schema errors. + +The `relay.config.json` defines two projects: + +```json +{ + "sources": { + "apps/console/src/pages/iam": "iam", + "apps/console/src": "core" + } +} +``` + +## Snapshot Mode + +Most list and detail pages support read-only snapshot viewing. This requires: + +1. **Duplicate routes** in the route file -- one for normal paths, one for `/snapshots/:snapshotId/...` paths +2. **Conditional rendering** -- check `Boolean(snapshotId)` from `useParams()` and hide all create/update/delete controls +3. **Adjusted URLs** -- breadcrumbs and internal links must use the snapshot path prefix + +Example from `apps/console/src/routes/assetRoutes.ts`: +```tsx +// Normal route +{ path: "assets", loader: loaderFromQueryLoader(({ organizationId }) => + loadQuery(coreEnvironment, assetsQuery, { organizationId, snapshotId: null })), +}, +// Snapshot route +{ path: "snapshots/:snapshotId/assets", loader: loaderFromQueryLoader(({ organizationId, snapshotId }) => + loadQuery(coreEnvironment, assetsQuery, { organizationId, snapshotId })), +}, +``` + +## Permission Fragments + +Inline permission queries in Relay fragments gate UI controls: + +```graphql +canCreate: permission(action: "core:risk:create") +canUpdate: permission(action: "core:risk:update") +canDelete: permission(action: "core:risk:delete") +``` + +These boolean fields control visibility of edit buttons, delete actions, and create dialogs. There are no separate permission hooks. + +## ViewerMembershipLayout + +`apps/console/src/pages/iam/organizations/ViewerMembershipLayout.tsx` is the top-level authenticated shell. It: +1. Wraps `IAMRelayProvider` for its own membership query +2. Mounts `CoreRelayProvider` for child organization pages +3. Provides `CurrentUser` context (email, fullName, role) to the entire app + +## Key Abstractions + +| Abstraction | File | Purpose | +|-------------|------|---------| +| `loaderFromQueryLoader` | `@probo/routes` | Bridges React Router loaders with Relay preloaded queries | +| `useMutationWithToasts` | `src/hooks/useMutationWithToasts.ts` | **DEPRECATED** -- use `useMutation` + `useToast` | +| `SortableTable` | `src/components/SortableTable.tsx` | Paginated, sortable list with refetch support | +| `OrganizationErrorBoundary` | `src/components/OrganizationErrorBoundary.tsx` | Catches auth/assumption errors and redirects | +| `useOrganizationId` | `src/hooks/useOrganizationId.ts` | Extracts `:organizationId` from route params | + +## Legacy: hooks/graph/ Directory + +The `src/hooks/graph/` directory contains shared Relay queries, fragments, and mutations (e.g., `RiskGraph.ts`, `AssetGraph.ts`). This is **legacy code** with `eslint-disable` comments for `relay/must-colocate-fragment-spreads`. New code must colocate all GraphQL operations in the consuming component file. diff --git a/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md b/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md new file mode 100644 index 0000000000..24086d926c --- /dev/null +++ b/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md @@ -0,0 +1,73 @@ +# Probo -- TypeScript Frontend -- apps/trust + +> Module-specific notes for `apps/trust` (`@probo/trust`) +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md) + +## Purpose + +A public-facing trust center React SPA (port 5174) that exposes an organization's compliance posture to external visitors. It is read-only except for the auth and NDA signing flows. Pages include Overview, Documents, Subprocessors, Updates, and NDA signing. + +## Single Relay Environment + +Unlike the console app, trust uses a single Relay environment (`consoleEnvironment`) pointing at `/api/trust/v1/graphql`. Defined in `apps/trust/src/providers/RelayProviders.tsx`. + +## Path Prefix Routing + +The trust app supports two base URL modes: + +| Mode | Base Path | How detected | +|------|-----------|-------------| +| Probo-hosted | `/trust/{slug}` | URL matches `/trust/[^/]+` | +| Custom domain | `/` | Default fallback | + +Detection happens in `apps/trust/src/routes.tsx` via `getBasename()`. The `createBrowserRouter` receives the detected basename. + +**Critical rule**: Every manually constructed URL must use `getPathPrefix()` from `apps/trust/src/utils/pathPrefix.ts`. Never hardcode `/trust/` or `/`. This applies to redirect URLs, continue URLs, and all navigation targets. See [pitfalls.md -- Trust Center Path Prefix](../pitfalls.md#13-hardcoding-paths-without-getpathprefix-appstrust). + +## Auth Flow + +External visitors who need to access gated documents go through a multi-step auth flow: + +1. `RootErrorBoundary` catches `UnAuthenticatedError` and redirects to `/connect` with a `?continue=` URL +2. `/connect` (ConnectPage) -- magic link or OIDC login +3. `/verify-magic-link` -- validates the magic link token +4. `/full-name` -- captured if `FullNameRequiredError` is thrown +5. `/nda` -- NDA signing if `NDASignatureRequiredError` is thrown + +After successful auth, `useRequestAccessCallback` in MainLayout reads `request-document-id` / `request-report-id` / `request-file-id` search params and fires the corresponding access-request mutation automatically (post-login replay). + +## NDA Signing + +`apps/trust/src/pages/NDAPage.tsx` renders the NDA PDF, records signing events, and polls Relay every 1500ms until the signature status reaches `COMPLETED` before redirecting. There is currently no timeout/max-retry strategy for this polling. + +## TrustCenterProvider + +`apps/trust/src/providers/TrustCenterProvider.tsx` stores the `currentTrustCenter` Relay data from `MainLayout` so child components can access it without prop-drilling. Sub-pages like `OverviewPage` read trust center data from `useOutletContext` (inherited from MainLayout), not from their own query. + +## Route Organization + +All routes are defined in a single `apps/trust/src/routes.tsx` file (no separate route slice files like console). Routes are grouped: + +- **Auth routes**: Wrapped in `AuthLayout` (connect, verify-magic-link, full-name) +- **Content routes**: Wrapped in `MainLayout` with `RootErrorBoundary` (overview, documents, subprocessors, updates) +- **Standalone routes**: NDA page, document detail page + +## Key Abstractions + +| Abstraction | File | Purpose | +|-------------|------|---------| +| `getPathPrefix` | `src/utils/pathPrefix.ts` | Computes URL prefix for Probo-hosted vs custom domain mode | +| `RootErrorBoundary` | `src/components/RootErrorBoundary.tsx` | Typed error catch + auth redirect with continue URL | +| `useRequestAccessCallback` | `src/hooks/useRequestAccessCallback.ts` | Post-login access request replay from URL search params | +| `TrustCenterProvider` | `src/providers/TrustCenterProvider.tsx` | React context for trust center data from layout | + +## Differences from Console + +| Aspect | Console | Trust | +|--------|---------|-------| +| Relay environments | 2 (core + IAM) | 1 (trust) | +| Mutations | Frequent (CRUD for all entities) | Rare (access requests, NDA signing) | +| Route files | Separate per domain | Single `routes.tsx` | +| Snapshot mode | Yes (read-only views) | No (always read-only) | +| Permission fragments | Yes (`canCreate`, `canUpdate`, `canDelete`) | No (public read-only) | +| Auth errors | `UnAuthenticatedError`, `AssumptionRequiredError` | `UnAuthenticatedError`, `FullNameRequiredError`, `NDASignatureRequiredError` | diff --git a/.claude/guidelines/typescript-frontend/module-notes/packages-emails.md b/.claude/guidelines/typescript-frontend/module-notes/packages-emails.md new file mode 100644 index 0000000000..66c7193c45 --- /dev/null +++ b/.claude/guidelines/typescript-frontend/module-notes/packages-emails.md @@ -0,0 +1,90 @@ +# Probo -- TypeScript Frontend -- packages/emails + +> Module-specific notes for `packages/emails` (`@probo/emails`) +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md) + +## Purpose + +React Email templates for all transactional emails sent by Probo. Templates are authored in TSX using `@react-email/components`, compiled to Go-template HTML files by a build script, and consumed by the Go mailer via `//go:embed dist` and `html/template` rendering. + +## Hybrid Architecture + +This package spans both stacks -- TSX templates on the TypeScript side, Go `Presenter` on the Go side: + +``` +Developer writes .tsx template + | +npm run build (scripts/build.ts) + | + v +dist/.html.tmpl + dist/.txt.tmpl + | +go:embed dist (emails.go) + | + v +Go Presenter.Render*() executes templates at send time +``` + +## Adding a New Email Template + +Changes required in **four places**: + +1. **New `.tsx` file** in `packages/emails/src/` -- wrap content in ``, embed Go template variables as JSX string literals +2. **New `.txt` file** in `packages/emails/templates/` -- plain-text fallback with the same Go template variables +3. **Entry in `scripts/build.ts`** `TemplateConfig` array -- maps slug to component +4. **New `Render*` method** in `packages/emails/emails.go` -- builds template data, executes both templates + +Missing any step causes a build failure or runtime panic. + +## Go Template Variables in JSX + +The fundamental encoding trick: Go template syntax is embedded as JSX string literals so it passes through React rendering unchanged. + +```tsx +// From packages/emails/src/Invitation.tsx + + {"{{.RecipientFullName}}"}, you have been invited to join {"{{.OrganizationName}}"}. + + +``` + +Go range loops: + +```tsx +{"{{range .FileNames}}"} +{"{{.}}"} +{"{{end}}"} +``` + +**Pitfall**: Writing `{{.Foo}}` bare in JSX will be treated as JSX expression syntax and cause a TypeScript parse error. Always wrap in `{'{{.Foo}}'}` or `` {`{{.Foo}}`} ``. + +## EmailLayout + +`packages/emails/src/components/EmailLayout.tsx` is the shared wrapper providing: +- Consistent container and header logo +- Greeting line (`Hi {{.RecipientFullName}},`) +- Footer with company address +- "Powered By Probo" branding +- Exported CSS-in-JS style constants (`button`, `bodyText`, `footerText`) + +All email templates compose inside ``. + +## Template Components Have No Props + +Templates accept no props. The build script renders each as a zero-argument function: `render(() => ConfirmEmail())`. All dynamic values are Go template placeholders embedded as literal strings. + +## Build Dependency + +The `dist/` directory must be built before the Go binary is compiled. `go:embed dist` is evaluated at Go compile time. `make build` orchestrates this, but running `go build` directly will fail if `dist/` is missing. + +## Key Files + +| File | Purpose | +|------|---------| +| `packages/emails/src/Invitation.tsx` | Canonical short template example | +| `packages/emails/src/components/EmailLayout.tsx` | Shared layout + style constants | +| `packages/emails/scripts/build.ts` | Build pipeline: TSX to `.html.tmpl` | +| `packages/emails/emails.go` | Go Presenter: template execution, asset URL resolution | +| `packages/emails/templates/invitation.txt` | Plain-text fallback example | diff --git a/.claude/guidelines/typescript-frontend/module-notes/packages-n8n-node.md b/.claude/guidelines/typescript-frontend/module-notes/packages-n8n-node.md new file mode 100644 index 0000000000..2e4acbce17 --- /dev/null +++ b/.claude/guidelines/typescript-frontend/module-notes/packages-n8n-node.md @@ -0,0 +1,69 @@ +# Probo -- TypeScript Frontend -- packages/n8n-node + +> Module-specific notes for `packages/n8n-node` (`@probo/n8n-nodes-probo`) +> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md) + +## Purpose + +An n8n community node package that exposes the Probo API as n8n workflow actions. It provides a single `INodeType` (Probo) with approximately 80 operation files covering CRUD for all major resources, plus a raw GraphQL execute escape hatch. + +## Architecture + +``` +nodes/Probo/ + Probo.node.ts # INodeType entry point + Probo.node.json # Node metadata + GenericFunctions.ts # HTTP transport layer (GraphQL requests, pagination) + actions/ + index.ts # Resource registry, dynamic dispatch + / + index.ts # Operation dropdown + re-exports + create.operation.ts # INodeProperties + execute function + get.operation.ts + getAll.operation.ts + update.operation.ts + delete.operation.ts +credentials/ + ProboApi.credentials.ts # Server URL + API key +``` + +## Adding a New Operation + +1. Create `.operation.ts` in `actions//` with exported `description` (INodeProperties[]) and `execute` function +2. Add the operation value to the resource's `index.ts` operation dropdown +3. Re-export the operation module from `index.ts` under a camelCase alias matching the operation value +4. If new resource: register it in `actions/index.ts` resource array + +## Key Conventions + +- **GraphQL queries are inline template literals** inside `execute()` -- never separate files +- **`displayOptions.show`** must be set on every `INodeProperty` to scope fields to their resource + operation +- **`additionalFields`** collection pattern for optional create/update fields +- **Options collection** for optional response shaping (toggling GraphQL fragment inclusion) +- **Cursor pagination**: `proboApiRequestAllItems` loops with page size 100 until `hasNextPage` is false + +## Transport Layer + +All HTTP calls funnel through `GenericFunctions.ts`: + +| Function | API | Purpose | +|----------|-----|---------| +| `proboApiRequest` | Console | Single GraphQL request | +| `proboConnectApiRequest` | Connect/IAM | Single GraphQL request | +| `proboApiRequestAllItems` | Console | Cursor-paginated list | +| `proboConnectApiRequestAllItems` | Connect/IAM | Cursor-paginated list | +| `proboApiMultipartRequest` | Console | File upload (multipart/form-data) | + +GraphQL-level errors (HTTP 200 with `response.errors`) are detected and converted to `NodeApiError` with `httpCode: '200'`. + +## Known Inconsistency: User Resource + +The `user` resource uses non-standard operation values: `createUser`, `getUser`, `listUsers`, `inviteUser`, `updateUser`, `updateMembership`, `removeUser` instead of the standard `create`, `get`, `getAll` pattern used by all other resources. The dispatch logic in `actions/index.ts` relies on exact key matching, so these longer names must match the export aliases in `user/index.ts`. + +## Publishing + +This package is published to npmjs.org as `@probo/n8n-nodes-probo` on tag push via CI. The version in `package.json` is currently `0.0.1`. + +## No Tests + +There are no test files in this package. Operations, GenericFunctions pagination, and error handling have zero automated coverage. diff --git a/.claude/guidelines/typescript-frontend/patterns.md b/.claude/guidelines/typescript-frontend/patterns.md new file mode 100644 index 0000000000..82ea245fac --- /dev/null +++ b/.claude/guidelines/typescript-frontend/patterns.md @@ -0,0 +1,284 @@ +# Probo -- TypeScript Frontend -- Patterns + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +## Relay Data Fetching + +See [shared.md -- Cross-Stack Contracts](../shared.md#cross-stack-contracts) for how GraphQL schemas flow from the Go backend to Relay-generated TypeScript types. + +### Relay Environments (module-specific: apps/console) + +The console app uses **two separate Relay environments** connecting to two GraphQL APIs: + +| Environment | Endpoint | Purpose | +|-------------|----------|---------| +| `coreEnvironment` | `/api/console/v1/graphql` | Main application data | +| `iamEnvironment` | `/api/connect/v1/graphql` | Authentication / identity | + +Defined in `apps/console/src/environments.ts`. Each has its own normalized store with a 1-minute query cache expiration. The `relay.config.json` maps source directories to Relay compiler projects: `src/pages/iam/` maps to the `iam` project, everything else maps to `core`. + +The trust app uses a single Relay environment (`consoleEnvironment`) pointing at `/api/trust/v1/graphql`, defined in `apps/trust/src/providers/RelayProviders.tsx`. + +### Colocated GraphQL Operations (universal) + +All GraphQL queries, fragments, and mutations are defined **inline** in the component file that uses them, using the `graphql` template tag from `relay-runtime`. There are no separate `.graphql` files on the frontend. + +```tsx +// Example from apps/trust/src/pages/DocumentPage.tsx +export const documentPageQuery = graphql` + query DocumentPageQuery($id: ID!) { + node(id: $id) @required(action: THROW) { + __typename + ... on Document { + id + title + isUserAuthorized + } + } + } +`; +``` + +The `hooks/graph/` directory in `apps/console` is **legacy** -- new code must colocate operations in the consuming component. This is enforced by the `relay/must-colocate-fragment-spreads` ESLint rule. + +### Fragment-Based Data Requirements (universal) + +Components declare their data needs via Relay fragments. Fragment names must match the component name: + +```tsx +// Example pattern from contrib/claude/relay.md +const contactFragment = graphql` + fragment VendorContactsTabFragment_contact on VendorContact { + id + fullName + email + canUpdate: permission(action: "core:vendor-contact:update") + canDelete: permission(action: "core:vendor-contact:delete") + } +`; +``` + +Never create custom TypeScript types for API data. All data shape types come from Relay-generated fragment types in `__generated__/` directories. This is enforced by a custom ESLint plugin (`@probo/eslint-plugin-relay-types`). + +### Route-Level Data Loading (universal) + +Both apps preload data in React Router loaders before rendering. There are two patterns, and the codebase is migrating from the older one to the newer one: + +**New pattern (preferred): Loader component** + +The route definition lazily imports a `*Loader` component that handles query loading internally: + +```tsx +// From apps/console/src/routes/documentsRoutes.ts +{ + path: "documents", + Fallback: PageSkeleton, + Component: lazy(() => import("#/pages/organizations/documents/DocumentsPageLoader")), +} +``` + +The Loader component uses `useQueryLoader` + `useEffect`: + +```tsx +// From apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx +function DocumentsPageQueryLoader() { + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader(documentsPageQuery); + + useEffect(() => { + if (!queryRef) { + loadQuery({ organizationId }); + } + }); + + if (!queryRef) return ; + return ; +} +``` + +**Old pattern (deprecated): `withQueryRef` HOC** (enforced in code review) + +```tsx +// From apps/console/src/routes/assetRoutes.ts -- DEPRECATED, do not use in new code +loader: loaderFromQueryLoader(({ organizationId }) => + loadQuery(coreEnvironment, assetsQuery, { + organizationId, + snapshotId: null, + }), +), +Component: withQueryRef( + lazy(() => import("#/pages/organizations/assets/AssetsPage")), +), +``` + +Code reviewers will block PRs that use `withQueryRef` -- use a Loader component instead. + +### Mutations (universal) + +Use `useMutation` from `react-relay` combined with `useToast` from `@probo/ui` for user feedback: + +```tsx +// Pattern from contrib/claude/relay.md +const { toast } = useToast(); +const [createObligation, isCreating] = useMutation(createMutation); + +const onSubmit = (formData: FormData) => { + createObligation({ + variables: { input: { ...formData }, connections: [connectionId] }, + onCompleted() { + toast({ title: __("Success"), description: __("Created"), variant: "success" }); + }, + onError(error) { + toast({ + title: __("Error"), + description: formatError(__("Failed"), error as GraphQLError), + variant: "error", + }); + }, + }); +}; +``` + +**Deprecated hooks -- do not use:** +- `useMutationWithToasts` -- use `useMutation` + `useToast` instead (enforced in code review) +- `promisifyMutation` -- use `useMutation` with `onCompleted`/`onError` callbacks + +### Relay Store Updates (universal) + +Use Relay directives for connection updates -- no manual store manipulation: + +- `@prependEdge(connections: $connections)` -- add to beginning of list +- `@appendEdge(connections: $connections)` -- add to end of list +- `@deleteEdge(connections: $connections)` -- remove from list +- Fragment spread on update mutations -- Relay auto-updates in place + +The `connections` variable comes from the `__id` field on the connection in the parent fragment. Use `ConnectionHandler.getConnectionID()` with matching filter args. + +### Destructive Actions (universal) + +Destructive mutations (delete) must be wrapped with `useConfirm` for a confirmation dialog before execution. + +### Pagination (universal) + +Cursor-based pagination uses `usePaginationFragment` with `@connection` directive. `SortableTable` from `apps/console/src/components/SortableTable.tsx` is the standard component for rendering paginated, sortable lists. + +## Routing + +### Route Definitions (universal) + +Routes are defined as arrays that satisfy `AppRoute[]` from `@probo/routes`. Each app assembles its routes differently: + +- **Console**: feature-specific route files in `src/routes/*.ts` (e.g., `riskRoutes.ts`, `assetRoutes.ts`), assembled in `src/routes.tsx` +- **Trust**: all routes defined in `src/routes.tsx` with `createBrowserRouter` + +All route arrays end with `satisfies AppRoute[]` for type safety. + +### Lazy Loading (universal) + +Page components are code-split using `lazy()` from `@probo/react-lazy`: + +```tsx +Component: lazy(() => import("#/pages/organizations/assets/AssetsPage")), +``` + +Pages use default exports (required by `React.lazy`). All other modules use named exports. + +### Error Boundaries (universal) + +Both apps use React Router `ErrorBoundary` components per route group: + +- **Console**: `RootErrorBoundary` at root, `OrganizationErrorBoundary` per organization scope +- **Trust**: `RootErrorBoundary` on every content route, `DocumentPageErrorBoundary` for document pages + +Error boundaries catch typed errors from `@probo/relay` and redirect accordingly: +- `UnAuthenticatedError` -- redirect to login +- `AssumptionRequiredError` -- redirect to organization assume page (console) +- `FullNameRequiredError` -- redirect to full-name page (trust) +- `NDASignatureRequiredError` -- redirect to NDA page (trust) + +See `apps/console/src/components/OrganizationErrorBoundary.tsx` and `apps/trust/src/components/RootErrorBoundary.tsx`. + +## Component Architecture + +### Design System Hierarchy (module-specific: packages/ui) + +`@probo/ui` follows an Atom/Molecule/Layout hierarchy: + +| Layer | Location | Examples | +|-------|----------|----------| +| Atoms | `packages/ui/src/Atoms/` | Button, Badge, Checkbox, Icons, Tabs, Spinner | +| Molecules | `packages/ui/src/Molecules/` | Dialog, EditableRow, RisksChart, Combobox | +| Layouts | `packages/ui/src/Layouts/` | Layout, CenteredLayout, ErrorLayout | + +Variant logic uses `tailwind-variants` (`tv()`) -- not `cn()` -- for components with variant props. See `packages/ui/src/Atoms/Badge/Badge.tsx` for the canonical atom pattern. + +### asChild / Slot Pattern (module-specific: packages/ui) + +Components accepting `asChild` delegate rendering to the child element via a local `Slot` component (`packages/ui/src/Atoms/Slot.tsx`), similar to Radix Slot. Always pass exactly one child when using `asChild`. + +### Radix UI Primitives (module-specific: packages/ui) + +Accessible headless primitives come from `@radix-ui/*` packages (Dialog, Dropdown, Popover, Select, Tabs, Label, ScrollArea). Never build these from scratch. + +## Forms + +### Validation (universal across apps) + +Forms use `react-hook-form` + `zod` schemas via the `useFormWithSchema` wrapper hook: + +```tsx +const form = useFormWithSchema(schema, { defaultValues }); +``` + +Zod schemas are the single source of truth for form data types -- use `z.infer` for type derivation. + +### Domain Helpers for Form Options (universal) + +`@probo/helpers` provides `getXOptions(__)` functions that map enum arrays to `{ value, label }[]` arrays ready for select/dropdown components. The `Translator` function (`__` from `useTranslate()`) is always the first argument. + +## Permission Checks (module-specific: apps/console) + +Permissions are fetched inline inside Relay fragments using the `permission(action:)` field: + +```graphql +canCreate: permission(action: "core:asset:create") +canUpdate: permission(action: "core:asset:update") +canDelete: permission(action: "core:asset:delete") +``` + +Components gate UI controls (edit buttons, delete actions) on these boolean fields from fragment data. There are no separate permission hooks. + +See [shared.md -- Review-Enforced Standards](../shared.md#review-enforced-standards) for the broader rule that access control belongs in ABAC policies, not UI conditionals. + +## Snapshot Mode (module-specific: apps/console) + +Most list and detail pages support a read-only snapshot mode activated by a `:snapshotId` route parameter. Pages check `Boolean(snapshotId)` to hide edit/delete controls and adjust breadcrumbs/URLs to use the `/snapshots/:snapshotId/...` prefix. Route files define duplicate routes for both normal and snapshot paths. + +See `apps/console/src/pages/organizations/risks/RiskDetailPage.tsx` for the correct pattern. + +## Error Handling + +### Relay Network Layer (universal) + +`@probo/relay` (`packages/relay/src/fetch.ts`) maps GraphQL error extension codes to typed JavaScript error classes. HTTP 500 throws `InternalServerError`. Only the first matching error code is thrown -- subsequent errors in the same response are ignored. + +See [shared.md -- GraphQL error code contract](../shared.md#cross-stack-contracts) for the full code-to-class mapping. + +### Mutation Error Handling (universal) + +Mutations handle errors in two callbacks: +- `onCompleted` -- for GraphQL-layer errors returned in `response.errors[]` +- `onError` -- for network/auth errors + +`formatError()` from `@probo/helpers` normalizes error objects into user-facing strings for toast notifications. + +### Toast Notifications (universal) + +User-facing errors surface through `useToast()` from `@probo/ui`, backed by a Zustand store. The `Toasts` component is mounted once inside `Layout` -- never add it again in child routes. + +## Observability + +The TypeScript frontend has **no structured logging, metrics, or tracing**. This is a deliberate architectural choice. Errors surface to users via Relay typed error classes and React error boundaries. A small number of `console.log`/`console.error` calls exist for ad-hoc debugging. + +See [shared.md -- Frontend observability](../shared.md#frontend-observability) for context. diff --git a/.claude/guidelines/typescript-frontend/pitfalls.md b/.claude/guidelines/typescript-frontend/pitfalls.md new file mode 100644 index 0000000000..a99d8eda9e --- /dev/null +++ b/.claude/guidelines/typescript-frontend/pitfalls.md @@ -0,0 +1,160 @@ +# Probo -- TypeScript Frontend -- Pitfalls + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +## Deprecated Patterns + +### 1. Using `withQueryRef` in route definitions + +**What goes wrong**: New route definitions using `withQueryRef` will be rejected in code review. This is listed as an approval blocker. + +**Why**: The team is migrating to the Loader component pattern, which gives better control over loading states, Relay environment wrapping, and Suspense boundaries. + +**How to avoid**: Create a `*Loader.tsx` component that uses `useQueryLoader` + `useEffect` internally. See `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` for the canonical example. + +**Source**: Code review (PR #845) -- reviewer explicitly stated "stop using withQueryRef and use a page Loader component." + +### 2. Using `useMutationWithToasts` + +**What goes wrong**: The hook couples mutation execution with toast display in a way the team considers inflexible. + +**Why**: The team prefers explicit composition of `useMutation` + `useToast` for clearer control flow and error handling. + +**How to avoid**: Import `useMutation` from `react-relay` and `useToast` from `@probo/ui` separately. Handle success/error in `onCompleted`/`onError` callbacks. + +**Source**: Code review (PR #855) -- "don't use useMutationWithToasts instead use useMutation and useToast." + +### 3. Using `promisifyMutation` from `@probo/helpers` + +**What goes wrong**: Wrapping Relay mutations in a Promise hides the callback-based API that Relay expects and makes error handling inconsistent. + +**How to avoid**: Use `useMutation` with `onCompleted`/`onError` callbacks directly. + +**Source**: `contrib/claude/relay.md` -- explicitly marked as deprecated. + +### 4. Putting GraphQL operations in `hooks/graph/` files + +**What goes wrong**: New files in `hooks/graph/` violate the `relay/must-colocate-fragment-spreads` ESLint rule and the project mandate for colocated operations. Existing files in this directory are legacy. + +**How to avoid**: Define all `graphql` tagged templates inline in the component file that uses them. + +**Source**: `apps/console/CLAUDE.md` -- "Queries, fragments, and mutations are colocated in the component that uses them -- never in separate hooks/graph/ files." + +## Relay Environment Mismatches + +### 5. Using wrong Relay environment for page area (apps/console) + +**What goes wrong**: Using `coreEnvironment` inside an IAM page (`src/pages/iam/`) or `iamEnvironment` inside an organization page causes schema mismatches. Queries will fail because the schema does not match the environment's GraphQL endpoint. + +**Why**: The Relay compiler maps `src/pages/iam/` to the `iam` project and everything else to `core`. The environments must match. + +**How to avoid**: IAM pages must use `IAMRelayProvider`. Organization pages must use `CoreRelayProvider`. The `relay.config.json` sources mapping is the authority: `"apps/console/src/pages/iam": "iam"`, `"apps/console/src": "core"`. + +**Source**: Codebase pattern -- `apps/console/relay.config.json`. + +## Relay Store Update Pitfalls + +### 6. Forgetting `@appendEdge`/`@deleteEdge` directives on mutations + +**What goes wrong**: The UI does not reflect the mutation result until a full page reload. Items appear to not be created or not be deleted. + +**Why**: Without Relay store directives, the normalized store is not updated and the list connection remains stale. + +**How to avoid**: Always include `@prependEdge(connections: $connections)` or `@deleteEdge(connections: $connections)` in mutation GraphQL. Pass the `connections` variable obtained from `__id` on the connection fragment. Ensure `ConnectionHandler.getConnectionID()` uses the same arguments as the `@connection` directive. + +**Source**: Codebase pattern -- see `apps/console/src/pages/organizations/risks/RiskDetailPage.tsx`. + +### 7. Only first GraphQL error code is thrown by `@probo/relay` + +**What goes wrong**: If a response contains multiple GraphQL errors, only the first recognized error code is thrown as a typed exception. All subsequent errors are silently ignored. + +**Why**: `packages/relay/src/fetch.ts` short-circuits on the first `errors.find(...)` match. + +**How to avoid**: Be aware that error boundaries will only see the first error. If you need to handle multiple error codes, inspect the raw response in the `onCompleted` callback. + +**Source**: Codebase pattern -- `packages/relay/src/fetch.ts` lines 85-108. + +## UI Component Pitfalls + +### 8. Calling `useEditableRowContext()` outside `EditableRow` + +**What goes wrong**: Runtime exception -- the hook throws if called outside an `EditableRow` context. There is no fallback null guard. + +**How to avoid**: Always wrap `EditableCell` and `SelectCell` inside an `` parent. + +**Source**: Codebase pattern -- `packages/ui/src/Molecules/Table/EditableRow.tsx`. + +### 9. Mounting `` more than once + +**What goes wrong**: Toasts render twice because the Zustand store is module-level shared state. + +**How to avoid**: `` is already mounted inside `` from `@probo/ui`. Never add it again in child routes or page components. + +**Source**: Codebase pattern -- `packages/ui/src/Atoms/Toasts/Toasts.tsx`. + +### 10. Missing `theme.css` import in consuming apps + +**What goes wrong**: All design-token-based Tailwind classes (e.g., `bg-level-0`, `text-txt-primary`) render unstyled. The page looks completely broken. + +**Why**: `@probo/ui` uses Tailwind CSS v4 design tokens defined in `packages/ui/src/theme.css`. Without importing it, the custom utility classes resolve to nothing. + +**How to avoid**: Ensure the consuming app imports the theme CSS. See `packages/ui/.storybook/preview.tsx` for the import pattern. + +**Source**: Codebase pattern -- `packages/ui`. + +### 11. Passing `tv()` factory instead of calling it + +**What goes wrong**: The `className` prop receives `[object Object]` instead of a class string. + +**Why**: `tailwind-variants` `tv()` returns a function that must be called with variant props. + +**How to avoid**: Always call the factory: `badge({ variant, size })`, not `badge`. + +**Source**: Codebase pattern -- `packages/ui/src/Atoms/Button/Button.tsx`. + +## Snapshot Mode Pitfalls + +### 12. Forgetting snapshot mode on new list/detail pages (apps/console) + +**What goes wrong**: Users can mutate data on read-only snapshot views. Create/update/delete controls remain visible when viewing a snapshot. + +**How to avoid**: Check `useParams()` for `snapshotId`. When present, hide all mutation controls and use `/snapshots/:snapshotId/...` URL prefix. Define duplicate routes in the route file for both normal and snapshot paths. + +**Source**: Codebase pattern -- see `apps/console/src/routes/assetRoutes.ts` for duplicate route definitions, and `apps/console/src/pages/organizations/risks/RiskDetailPage.tsx` for conditional rendering. + +## Trust Center Path Prefix + +### 13. Hardcoding paths without `getPathPrefix()` (apps/trust) + +**What goes wrong**: URLs break in Probo-hosted mode where the app runs at `/trust/{slug}` instead of `/`. + +**Why**: The trust app supports two base URL modes. Hardcoding `/` as the path prefix breaks when the basename is `/trust/{slug}`. + +**How to avoid**: Always use `getPathPrefix()` from `apps/trust/src/utils/pathPrefix.ts` when constructing redirect or continue URLs manually. Never hardcode `/trust/` strings. + +**Source**: Codebase pattern -- every file that constructs a full URL in `apps/trust`. + +## Custom TypeScript Types for API Data + +### 14. Hand-writing interfaces that mirror GraphQL types + +**What goes wrong**: Types drift from the schema. TypeScript compiles but the runtime data shape may not match. + +**Why**: Relay codegen produces precise types from the schema. Hand-written types duplicate this and inevitably go stale. + +**How to avoid**: Always use Relay-generated fragment types from `__generated__/`. The custom `@probo/eslint-plugin-relay-types` enforces this. + +**Source**: `apps/console/CLAUDE.md` -- "Never create custom TypeScript types for API data." + +## Access Control in UI + +### 15. Putting state-based access control in UI conditionals + +**What goes wrong**: The frontend hides a button, but the API still accepts the mutation. Or a different frontend path (MCP, CLI) bypasses the check entirely. + +**Why**: Access control must be enforced in the backend ABAC policy system (`pkg/probo/policies.go`), not via frontend visibility toggles. The frontend permission fragments (`canCreate`, `canUpdate`, `canDelete`) reflect ABAC decisions -- they do not replace them. + +**How to avoid**: If a resource's state (e.g., archived) should prevent an operation, add the condition to the ABAC policy, not to a JSX conditional. + +**Source**: Code review (PR #855, frequency 3) -- "should we consider putting this logic in our ABAC." See [shared.md -- Review-Enforced Standards](../shared.md#review-enforced-standards). diff --git a/.claude/guidelines/typescript-frontend/testing.md b/.claude/guidelines/typescript-frontend/testing.md new file mode 100644 index 0000000000..9c7867065d --- /dev/null +++ b/.claude/guidelines/typescript-frontend/testing.md @@ -0,0 +1,158 @@ +# Probo -- TypeScript Frontend -- Testing + +> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 +> Cross-cutting conventions: see [shared.md](../shared.md) + +## Testing Strategy Overview + +The TypeScript frontend has a layered testing strategy: + +| Layer | Framework | Scope | Location | +|-------|-----------|-------|----------| +| Component stories | Storybook 10 + `@storybook/addon-vitest` | Visual/interactive testing of UI atoms and molecules | `packages/ui/src/**/*.stories.tsx` | +| Unit tests | Vitest | Pure utility functions and helpers | `packages/helpers/src/**/*.test.ts` | +| End-to-end tests | Go test suite | Full-stack integration against live `bin/probod` | `e2e/console/` (Go, not TypeScript) | + +There are **no unit or component tests** inside the app directories (`apps/console/`, `apps/trust/`). Frontend behavior is validated through Storybook stories for UI components and Go end-to-end tests for full user flows. + +## Storybook (packages/ui) + +### Setup + +Storybook 10 with `@storybook/addon-vitest` is configured in `packages/ui/.storybook/`. Stories serve as the primary test surface for the design system. + +### Story File Convention + +Story files are co-located next to the component: + +``` +packages/ui/src/Atoms/Button/ + Button.tsx + Button.stories.tsx +``` + +### Story Structure + +```tsx +// From packages/ui/src/Atoms/Button/Button.stories.tsx +import type { Meta, StoryObj } from "@storybook/react"; + +import { IconPlusLarge } from "../Icons"; +import { Button } from "./Button"; + +export default { + title: "Atoms/Button", + component: Button, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + render() { + return ( +
+ {variants.map(variant => ( +
+ +
+ ))} +
+ ); + }, +}; +``` + +Key conventions: +- `export default` uses `satisfies Meta` for type safety +- Story type is `StoryObj` +- Titles follow the Atom/Molecule hierarchy: `"Atoms/Button"`, `"Molecules/Dialog"` +- The `render()` function demonstrates all variants +- A `BrowserRouter` decorator is configured in `.storybook/preview.tsx` for components that use `react-router` + +### Running Storybook + +```sh +cd packages/ui +npm run storybook # Start dev server +npm run test-storybook # Run story tests via vitest +``` + +## Vitest (packages/helpers, packages/hooks, packages/relay) + +### Setup + +Vitest is configured as a dev dependency in `packages/helpers`, `packages/hooks`, and `packages/relay`. Currently, only `packages/helpers` has actual test files. The other packages have vitest configured but no tests written. + +### Test File Convention + +Test files are co-located next to the source file: + +``` +packages/helpers/src/ + file.ts + file.test.ts + array.ts + array.test.ts +``` + +### Test Structure + +```ts +// From packages/helpers/src/file.test.ts +import { describe, it, expect } from "vitest"; +import { isEmpty } from "./array"; +import { fileSize } from "./file"; + +describe("file", () => { + it("should display a file size in a human readable format", () => { + const fakeTranslator = (s: string) => s; + expect(fileSize(fakeTranslator, 4911)).toMatchInlineSnapshot( + `"4.8 KB"`, + ); + expect(fileSize(fakeTranslator, 20)).toMatchInlineSnapshot(`"20 B"`); + }); +}); +``` + +Key conventions: +- `describe` blocks named after the module +- `it` blocks describe the expected behavior +- **Fake translator pattern**: `const fakeTranslator = (s: string) => s` -- passes strings through unchanged for testing label functions +- **`toMatchInlineSnapshot`** for formatted output assertions -- keeps expected values visible in the test file +- No setup/teardown utilities -- tests are simple and self-contained + +### Running Tests + +```sh +cd packages/helpers +npx vitest run # Run all tests +npx vitest # Watch mode +``` + +## End-to-End Tests + +Frontend user flows are tested through Go end-to-end tests in `e2e/console/`. These run against a live `bin/probod` instance with a full Docker Compose stack. The tests exercise the GraphQL API that the frontend consumes, not the React UI directly. + +See [shared.md -- CI/CD Pipeline](../shared.md#cicd-pipeline) for how e2e tests run in CI. See `contrib/claude/e2e.md` in the repository for the full Go e2e testing guide. + +## Test Coverage Gaps + +The following areas have no automated test coverage: + +| Area | Impact | +|------|--------| +| `apps/console` components and pages | No unit/integration tests; covered only by Go e2e tests | +| `apps/trust` components and pages | No unit/integration tests | +| `packages/relay` error-code mapping and upload logic | vitest configured but zero test files | +| `packages/hooks` all hooks | vitest configured but zero test files | +| `packages/emails` template rendering | Build script acts as a smoke test but no assertions | +| `packages/n8n-node` operations and pagination | No tests at all | + + +## Notes on Test Strategy + +_Reserved for team notes on testing priorities and plans -- this section is preserved on refresh._ + From b5c707a6b34c72320051677f331c85369312e5d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:49:54 +0200 Subject: [PATCH 4/9] Add AI coding skills for Probo codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/skills/potion-ask/SKILL.md | 144 +++++++ .claude/skills/potion-implement/SKILL.md | 143 +++++++ .claude/skills/potion-plan/SKILL.md | 503 +++++++++++++++++++++++ .claude/skills/potion-review/SKILL.md | 259 ++++++++++++ 4 files changed, 1049 insertions(+) create mode 100644 .claude/skills/potion-ask/SKILL.md create mode 100644 .claude/skills/potion-implement/SKILL.md create mode 100644 .claude/skills/potion-plan/SKILL.md create mode 100644 .claude/skills/potion-review/SKILL.md diff --git a/.claude/skills/potion-ask/SKILL.md b/.claude/skills/potion-ask/SKILL.md new file mode 100644 index 0000000000..296403a46e --- /dev/null +++ b/.claude/skills/potion-ask/SKILL.md @@ -0,0 +1,144 @@ +--- +name: potion-ask +description: > + Answers questions about the Probo codebase across both Go backend and + TypeScript frontend stacks. Use when someone asks "where is...", "how does + X work", "why is Y done this way", "what pattern does Z use", "explain the + architecture", "find the code that handles...", "what does this module do", + or any question about understanding this project. Also triggers for + onboarding questions like "how do I get started", "what should I know", + "walk me through the codebase", or "how are the stacks connected". +allowed-tools: Read, Glob, Grep +model: sonnet +effort: medium +--- + +# Probo -- Codebase Q&A + +Before answering, read the guidelines at `.claude/guidelines/` -- start with +`shared.md` for cross-stack conventions, then check the relevant stack's +`index.md` for architecture overview and topic files (`patterns.md`, +`conventions.md`, `pitfalls.md`, `testing.md`). + +## Stack routing + +Determine which stack the question targets before exploring: + +| Signal | Stack | Guidelines path | +|--------|-------|----------------| +| Go, `pkg/`, `cmd/`, `e2e/`, coredata, GraphQL resolvers, MCP, CLI, service layer, IAM, SCIM, SAML | Go Backend | `.claude/guidelines/go-backend/` | +| TypeScript, React, Relay, `apps/`, `packages/`, frontend, UI, components, hooks, Vite | TypeScript Frontend | `.claude/guidelines/typescript-frontend/` | +| GraphQL schema, GID, cross-stack, API contract, deployment, CI/CD, license | Shared | `.claude/guidelines/shared.md` | + +## Answering strategy + +1. **Check guidelines first.** Most architecture and pattern questions are + already answered there. Do not explore what is already documented. +2. **Locate the module.** Use the module map below to narrow scope. +3. **Explore with precision.** Grep and Glob to find specific code. Read + files to confirm. Never say "it is probably in..." -- find it. +4. **Cite your sources.** Reference specific files and line numbers. + +## Module map -- Go Backend + +| Module | Path | Purpose | +|--------|------|---------| +| cmd | `cmd/` | Binary entrypoints: `probod`, `prb`, `probod-bootstrap`, `acme-keygen` | +| pkg/server | `pkg/server/` | HTTP server, chi router, middleware stack, all API surface handlers | +| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen) -- 80+ type mapping files | +| pkg/server/api/trust/v1 | `pkg/server/api/trust/v1/` | Trust center GraphQL API -- NDA directive, read-only | +| pkg/server/api/connect/v1 | `pkg/server/api/connect/v1/` | IAM GraphQL API + SAML/OIDC/SCIM handlers | +| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) -- AI-agent access to domain objects | +| pkg/probo | `pkg/probo/` | Core business logic -- 40+ domain sub-services | +| pkg/iam | `pkg/iam/` | Identity and access management, ABAC policy evaluation | +| pkg/iam/policy | `pkg/iam/policy/` | Pure in-memory IAM policy evaluator | +| pkg/iam/scim | `pkg/iam/scim/` | SCIM 2.0 provisioning bridge | +| pkg/trust | `pkg/trust/` | Public trust center service layer | +| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | +| pkg/validator | `pkg/validator/` | Fluent validation framework | +| pkg/gid | `pkg/gid/` | 192-bit tenant-scoped entity identifiers | +| pkg/agent | `pkg/agent/` | LLM agent orchestration framework | +| pkg/llm | `pkg/llm/` | Provider-agnostic LLM abstraction | +| pkg/cmd | `pkg/cmd/` | CLI commands for `prb` tool (cobra) | +| e2e | `e2e/` | End-to-end integration tests | + +## Module map -- TypeScript Frontend + +| Module | Path | Purpose | +|--------|------|---------| +| apps/console | `apps/console/` | Admin dashboard SPA (React + Relay, port 5173) | +| apps/trust | `apps/trust/` | Public trust center SPA (React + Relay, port 5174) | +| packages/ui | `packages/ui/` | Shared design system (Tailwind Variants, Radix primitives) | +| packages/relay | `packages/relay/` | Relay FetchFunction factory + typed error classes | +| packages/helpers | `packages/helpers/` | Domain formatters, enum labels/variants, utility functions | +| packages/hooks | `packages/hooks/` | Shared React hooks | +| packages/emails | `packages/emails/` | React Email templates for transactional emails | +| packages/n8n-node | `packages/n8n-node/` | n8n community node for Probo API | + +## Canonical examples + +These files represent "the right way" in this project: + +| File | Demonstrates | +|------|-------------| +| `pkg/coredata/asset.go` | Complete coredata entity: LoadByID, Insert, Update, Delete, CursorKey, AuthorizationAttributes, Snapshot | +| `pkg/probo/vendor_service.go` | Service layer pattern: Request struct, Validate(), pg.WithTx, webhook in same tx | +| `pkg/server/api/console/v1/v1_resolver.go` | GraphQL resolver: authorize, ProboService, service call, type mapping | +| `pkg/iam/policy/example_test.go` | Policy DSL: Allow/Deny helpers, ABAC conditions, Go example tests | +| `e2e/console/vendor_test.go` | E2E test: factory builders, RBAC, tenant isolation, timestamp assertions | +| `apps/console/src/routes/documentsRoutes.ts` | New-style route definitions using Loader components (no withQueryRef) | +| `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` | Canonical Loader component: useQueryLoader + useEffect + Suspense | +| `apps/trust/src/pages/DocumentPage.tsx` | Full page with colocated queries/mutations, typed error handling | +| `packages/ui/src/Atoms/Badge/Badge.tsx` | Canonical UI atom: tv() variant factory, asChild/Slot, typed props | +| `packages/helpers/src/audits.ts` | Domain helper: as const enum array, getXLabel with Translator, getXVariant | + +## How to handle different question types + +**"Where is X?"** -- Grep, check module map, return exact file + lines. + +**"How does X work?"** -- Find entry point, trace data flow through layers, +explain each step with file references. + +**"Why is X done this way?"** -- Check guidelines for rationale, then +`contrib/claude/` reference docs, then code comments. If no rationale exists, +say so -- do not invent. + +**"What pattern for X?"** -- Reference the relevant stack's patterns in +guidelines. Point to the canonical example that best matches. + +**"How do I get started?"** -- Walk through: structure (shared.md) -> +architecture (stack index.md) -> patterns -> canonical examples -> +`make build` / `make test`. + +**"How are the stacks connected?"** -- Explain the GraphQL schema contract +from shared.md. The Go backend authors `.graphql` schema files, gqlgen +generates Go resolvers, and the Relay compiler generates TypeScript types +from the same `.graphql` files. + +## Key patterns quick reference -- Go Backend + +- **Two-level service tree**: `Service` (global) -> `WithTenant(tenantID)` -> `TenantService` with sub-services +- **Request struct + Validate()**: every mutating method takes a `*Request` with fluent validation +- **All SQL in pkg/coredata only**: no other package may contain SQL queries +- **pgx.StrictNamedArgs**: never NamedArgs (approval blocker) +- **Error wrapping**: `fmt.Errorf("cannot : %w", err)` (never "failed to") +- **Scoper for tenant isolation**: entity structs have no TenantID field +- **Three interfaces**: every feature must have GraphQL + MCP + CLI endpoints + +## Key patterns quick reference -- TypeScript Frontend + +- **Relay colocated operations**: all GraphQL in component files, not `hooks/graph/` +- **Loader component pattern**: `useQueryLoader` + `useEffect` (not deprecated `withQueryRef`) +- **tv() for variants**: tailwind-variants, not cn() +- **useMutation + useToast**: not deprecated `useMutationWithToasts` +- **Permission fragments**: `canCreate: permission(action: "core:asset:create")` +- **Dual Relay environments** (console): `coreEnvironment` + `iamEnvironment` + +## Rules + +- Never guess. If you cannot find it, say so and suggest where to look. +- Prefer code snippets from the actual codebase over abstract descriptions. +- Note migrations or inconsistencies when relevant (e.g., "module X uses the + old pattern, the rest of the codebase uses the new one"). +- Keep answers focused. Answer the question, then offer to go deeper. +- For complex exploration, delegate to the explorer agent. diff --git a/.claude/skills/potion-implement/SKILL.md b/.claude/skills/potion-implement/SKILL.md new file mode 100644 index 0000000000..6f9ab59a25 --- /dev/null +++ b/.claude/skills/potion-implement/SKILL.md @@ -0,0 +1,143 @@ +--- +name: potion-implement +description: > + Master implementation orchestrator for Probo. Analyzes tasks, determines + which language stacks are involved (Go backend, TypeScript frontend, or + both), and delegates to stack-specific implementer agents. For cross-stack + tasks, orchestrates sequentially -- upstream first, then downstream with + actual changes as context. Use when someone asks to "add", "create", + "build", "implement", "write", or "code" anything. Also triggers for + tickets, specs, feature descriptions, or any request to make code changes. +allowed-tools: Read, Glob, Grep, Agent +model: opus +effort: high +--- + +# Probo -- Master Implementation Orchestrator + +This skill does NOT implement code itself. It analyzes incoming tasks, +determines which stack(s) are involved, and delegates to the right +stack-specific implementer agent(s). + +## Load guidelines + +Before analyzing any task, read the shared guidelines and every stack's index: + +- **Shared conventions:** `.claude/guidelines/shared.md` +- **Go Backend:** `.claude/guidelines/go-backend/index.md` +- **TypeScript Frontend:** `.claude/guidelines/typescript-frontend/index.md` + +## Stack routing table + +Use this table to map modules and file paths to their owning stack. + +### Go Backend (Go 1.26) +- **Frameworks:** chi router, gqlgen, mcpgen, pgx, testify +- **Modules:** cmd, pkg/server, pkg/probo, pkg/iam, pkg/trust, pkg/coredata, pkg/validator, pkg/gid, pkg/agent, pkg/llm, pkg/agents, pkg/cmd, pkg/cli, pkg/certmanager, pkg/webhook, pkg/mailer, pkg/slack, pkg/connector, pkg/bootstrap, e2e +- **Implementer agent:** `probo-go-backend-implementer` +- **Guidelines:** `.claude/guidelines/go-backend/` + +### TypeScript Frontend (React 19 + Relay 19) +- **Frameworks:** React, Relay, React Router v7, Tailwind CSS v4, Vite, Storybook 10 +- **Modules:** apps/console, apps/trust, packages/ui, packages/relay, packages/helpers, packages/hooks, packages/emails, packages/n8n-node, packages/routes, packages/coredata, packages/i18n +- **Implementer agent:** `probo-typescript-frontend-implementer` +- **Guidelines:** `.claude/guidelines/typescript-frontend/` + +## Task analysis + +For every incoming task, run through these steps before spawning any agent: + +1. **Read the task description.** Understand what is being asked -- feature, + bugfix, refactor, migration, etc. +2. **Identify affected modules.** Look for file paths, feature names, module + names, or domain concepts that map to known modules. +3. **Map modules to stacks** using the routing table above. Each module belongs + to exactly one stack. +4. **Classify the task:** + - **Single-stack** -- all affected modules belong to one stack. + - **Cross-stack** -- affected modules span both Go backend and TypeScript frontend. + +## Critical rule: three-interface sync + +Every new feature must be exposed through all three interfaces and kept in sync: + +1. **GraphQL** -- `pkg/server/api/console/v1/schema.graphql` (+ codegen) +2. **MCP** -- `pkg/server/api/mcp/v1/specification.yaml` (+ codegen) +3. **CLI** -- `pkg/cmd/` + +If the task adds a new domain entity or mutation, ensure all three are planned. +Every new Go API endpoint must also have end-to-end tests in `e2e/`. + +## Single-stack delegation + +When only one stack is involved: + +1. Spawn the appropriate implementer agent with the full task description. +2. Let it handle the implementation end-to-end. +3. No further orchestration needed. + +## Cross-stack orchestration + +When both stacks are involved, order matters. Implement upstream before +downstream so that downstream agents can reference the actual changes. + +### Step-by-step + +1. **Determine dependency order** using the direction rules below. +2. **Spawn the upstream implementer first.** Pass it the full task description + scoped to its stack. Wait for it to complete. +3. **Read upstream changes.** After the upstream agent finishes, read the files + it created or modified. Extract the contract -- GraphQL schema changes, + response types, function signatures. +4. **Spawn the downstream implementer** with upstream context: + ``` + "The Go backend implementer created {summary of changes}. + Here are the relevant details: {schema changes, new types, etc.} + Now implement the TypeScript frontend part that integrates with these changes." + ``` +5. **Verify coherence.** After both agents finish, check that the downstream + implementation actually uses the upstream contract correctly -- matching + GraphQL operations, field names, type shapes, etc. + +### Dependency direction rules + +| Task type | Order | Reasoning | +|-----------|-------|-----------| +| New API + frontend page | Go Backend then TypeScript Frontend | Frontend consumes the GraphQL API | +| Frontend form + backend validation | Go Backend then TypeScript Frontend | Validation defines constraints | +| Schema change + UI update | Go Backend then TypeScript Frontend | Schema generates types | +| Independent changes | Parallel | No dependency | +| Bug fix in one stack | Single stack only | No cross-stack coordination | + +### Cross-stack contract: the GraphQL schema + +The primary contract between stacks is the `.graphql` schema file: + +1. Go backend edits `pkg/server/api/console/v1/schema.graphql` +2. `go generate` regenerates Go resolver stubs and types (gqlgen) +3. Go developer implements resolver bodies +4. `npm run relay` regenerates TypeScript types from the same `.graphql` file +5. Frontend components consume generated types via Relay fragments + +After the Go implementer modifies the schema, tell the TypeScript implementer +to run `npm run relay` and use the generated types. + +## When the stack is unclear + +If the task description does not clearly map to any stack in the routing table, +do NOT guess. Ask the user: + +> "This task could touch the Go backend or the TypeScript frontend. Which +> stack(s) should I target?" + +## Post-orchestration checklist + +After all implementer agents have finished: + +- [ ] Every affected stack had its implementer agent spawned +- [ ] Cross-stack contracts are coherent (GraphQL types match, endpoints align) +- [ ] No orphaned references (e.g., frontend calling an API that was not created) +- [ ] Shared conventions from guidelines were respected across all stacks +- [ ] If a new entity was added: GraphQL + MCP + CLI all present +- [ ] If Go API changed: e2e tests in `e2e/` were created or updated +- [ ] ISC license headers present on all new files diff --git a/.claude/skills/potion-plan/SKILL.md b/.claude/skills/potion-plan/SKILL.md new file mode 100644 index 0000000000..e165d5e18e --- /dev/null +++ b/.claude/skills/potion-plan/SKILL.md @@ -0,0 +1,503 @@ +--- +name: potion-plan +description: > + Plans feature implementations, refactors, and architectural changes in + Probo across both Go backend and TypeScript frontend stacks. Identifies + which stacks are involved, determines execution order based on data flow, + and creates stack-labeled implementation sections. Use when someone asks + to "plan", "design", "break down", "spec out", "architect", or "how + should I implement" something. Also triggers for tickets, user stories, + feature requests, or specs that need an implementation approach. Even + "what files would I need to change for X" or "what is the best approach + for X" should activate this skill. +allowed-tools: Read, Write, Glob, Grep, AskUserQuestion, Agent, TodoWrite +model: opus +effort: high +--- + +# Probo -- Multi-Stack Implementation Planning + +Before planning, load shared conventions and each stack's architecture: +- `.claude/guidelines/shared.md` for cross-cutting conventions +- `.claude/guidelines/go-backend/index.md` for Go Backend architecture +- `.claude/guidelines/typescript-frontend/index.md` for TypeScript Frontend architecture + +## When to use this skill + +- Planning a new feature that may span stacks +- Designing an architecture change +- Breaking down a large task into stack-aware steps + +Use this BEFORE the implement skill. Planning catches architectural mistakes +when they are cheapest to fix -- before any code is written. + +--- + +## Phase 0: Pre-planning -- Gather context + +Before designing anything, understand the requirement deeply. Skipping this +phase is the number one cause of plans that miss the mark. + +### 1. Classify the task type + +Determine which kind of change this is -- it shapes the entire planning approach: + +| Type | Signals | Planning focus | +|------|---------|---------------| +| **New feature** | "add", "create", "build", "new" | Entry point, data flow, stacks involved, API contracts | +| **Refactor** | "refactor", "extract", "move", "rename", "split" | Migration path, backward compat, cross-stack contracts | +| **Bug fix** | "fix", "broken", "does not work", "regression" | Root cause vs. symptoms, which stack owns the bug | +| **Migration** | "upgrade", "migrate", "replace", "switch to" | Rollback strategy, incremental steps, feature parity | + +### 2. Explore the codebase + +Before asking questions, do your homework: + +- **Read relevant code** in each potentially affected stack. Grep for related + functionality. Understand what exists before proposing what to build. +- **Check cross-stack contracts.** The GraphQL schema files are the primary + contract between Go backend and TypeScript frontend. Read the relevant + `.graphql` file in `pkg/server/api/*/v1/`. +- **Check recent changes.** Look at recent commits in the affected areas. +- **Identify unknowns.** Note what you could not determine from the code alone. + +### 3. Ask targeted clarifying questions + +Use `AskUserQuestion` to surface ambiguity. Only ask questions whose answers +you could NOT determine from the code. Focus on: + +- **Acceptance criteria** -- What does "done" look like? What behaviors are expected? +- **Scope boundaries** -- What is explicitly out of scope? +- **Constraints** -- Performance requirements? Backward compatibility? Deadlines? +- **Edge cases** -- How should the system behave in non-happy-path scenarios? +- **Prior decisions** -- Has this been attempted before? Any rejected approaches? +- **Stack preferences** -- Should both stacks change, or should one adapt to the other? + +**Skip this step** if the requirement is already clear and specific (e.g., a +well-written ticket with acceptance criteria, or a trivial change). + +--- + +## Phase 1: Design the plan + +### 1. Restate the requirement + +Write a clear, specific summary. This is your contract: +- What is being built or changed? +- What is the expected user-facing behavior? +- What are the acceptance criteria (explicit or gathered in Phase 0)? + +### 2. Identify stacks involved + +Which stacks are affected by this change? Read each stack's module map and +guidelines to determine whether it participates: + +- **Go Backend** (Go 1.26) -- modules: cmd, pkg/server, pkg/probo, pkg/iam, pkg/trust, pkg/coredata, pkg/validator, pkg/gid, pkg/agent, pkg/llm, pkg/cmd, e2e +- **TypeScript Frontend** (React 19 + Relay 19) -- modules: apps/console, apps/trust, packages/ui, packages/relay, packages/helpers, packages/hooks + +### 3. Determine execution order + +Which stack is upstream (provides data/API) vs downstream (consumes)? +Order implementation so dependencies are built before consumers. + +| Task type | Order | Reasoning | +|-----------|-------|-----------| +| New API + frontend page | Go Backend -> TypeScript Frontend | Frontend consumes the GraphQL API | +| Frontend form + backend validation | Go Backend -> TypeScript Frontend | Validation defines constraints | +| Independent changes | Parallel | No dependency | +| Shared type change | Schema -> Go Backend -> TypeScript Frontend | Types flow downstream | +| Database migration + API update | Go Backend -> TypeScript Frontend | Schema change flows up | + +### 4. Reference stack-specific patterns + +For each affected stack, consult its patterns and conventions: + +For Go Backend work: see `.claude/guidelines/go-backend/patterns.md` +For TypeScript Frontend work: see `.claude/guidelines/typescript-frontend/patterns.md` + +### 5. Identify cross-stack integration points + +- The GraphQL schema file is the contract (e.g., `pkg/server/api/console/v1/schema.graphql`) +- Custom scalars must agree: GID (string), Datetime (string), CursorKey (string) +- GraphQL error codes map to typed exceptions in `@probo/relay` +- Relay compiler reads Go-side `.graphql` files directly via relative path + +### 6. Design the approach (by task type) + +#### For new features +1. Identify the entry point in each stack +2. Trace the data flow across stack boundaries via the GraphQL schema +3. Define the API contract (new types, mutations, queries in `.graphql`) +4. For Go: plan coredata entity + service + resolver + MCP tool + CLI command + e2e test +5. For TypeScript: plan Relay queries/fragments, page component, Loader, route +6. Plan integration tests that verify cross-stack behavior + +#### For refactors +1. Identify all files affected across stacks (Grep for usage) +2. Design the migration path -- can stacks be migrated independently? +3. Define backward compatibility strategy for cross-stack contracts +4. Plan: update schema first, then Go resolvers, then regenerate Relay types, then update TS + +#### For bug fixes +1. Determine which stack owns the root cause (not just where the symptom appears) +2. Plan the minimal fix in the owning stack +3. If the fix changes a contract, plan downstream stack updates +4. Plan regression tests (e2e for Go, Storybook for UI components) + +#### For migrations +1. Define feature parity across all affected stacks +2. Plan rollback strategy for each stack independently +3. Design incremental migration: one stack at a time when possible +4. Plan for contract coexistence (old and new API versions) + +### 7. Check pitfalls per stack + +These are real issues found in this codebase -- check each one against your plan: + +**Go Backend pitfalls:** +- Using `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` (approval blocker) +- Conditional string building in `SQLFragment()` (approval blocker) +- Error messages starting with "failed to" instead of "cannot" (approval blocker) +- Missing `t.Parallel()` in e2e subtests (approval blocker) +- Using `panic` in GraphQL resolvers (approval blocker -- MCP is the exception) +- Missing node resolver for types implementing Node +- Forgetting to register new entity type in `NewEntityFromID` switch + +**TypeScript Frontend pitfalls:** +- Using `withQueryRef` in route definitions (approval blocker -- use Loader component) +- Using `useMutationWithToasts` (deprecated -- use `useMutation` + `useToast`) +- Wrong Relay environment for page area (IAM pages use `iamEnvironment`) +- Forgetting `@appendEdge`/`@deleteEdge` on mutations +- Hardcoding paths without `getPathPrefix()` in apps/trust +- Hand-writing TypeScript interfaces that mirror GraphQL types + +--- + +## Phase 2: Produce the plan + +### File structure mapping + +Before defining steps, map every file that will be created or modified. +This locks in decomposition decisions before writing steps. + +For each file: +- **Path** -- verified with Glob (never guessed) +- **Action** -- create, modify, or delete +- **Responsibility** -- one clear purpose +- **Based on** -- canonical example it follows + +Follow codebase conventions for file organization. Files that change +together should live together. Split by responsibility, not by layer. + +### Step granularity + +Each step must be a **single, concrete action** completable in 2-5 minutes. + +**Bad step:** "Implement the service layer" +**Good step:** "Create `pkg/probo/widget_service.go` with the +`CreateWidget` method following the pattern in +`pkg/probo/vendor_service.go:45-80`" + +Each step must include: +- **Exact file path** (verified with Glob/Grep -- never guessed) +- **What to do** (create, modify specific lines, delete, wire up) +- **Code** -- actual code or detailed pseudo-code for the change. If the + step creates a file, show the file contents. If it modifies a file, show + the before/after or the new code to insert. Never write "follow pattern X" + without also showing what the resulting code looks like. +- **Verification** (exact command to run and expected output) + +### Plan output format + +``` +# Plan: {feature name} + +> Implement with `/potion-implement`. Track progress with TodoWrite. + +**Goal:** {one sentence: what this achieves} +**Type:** {Feature | Refactor | Bug fix | Migration} +**Tech:** {key technologies, libraries, or frameworks involved} + +### Summary +{2-3 sentences: what this plan achieves and why this approach} + +### Acceptance criteria +- [ ] {Criterion 1 -- specific, testable} +- [ ] {Criterion 2} + +### Stacks involved +| Stack | Role | Why needed | +|-------|------|-----------| + +### Execution order +{Which stack goes first and why -- justified by data flow direction} + +## Go Backend (Go 1.26) + +### File structure +| File | Action | Responsibility | Based on | +|------|--------|---------------|----------| + +### Delivery stages + +Group steps into stages. Each stage delivers working, testable software. +Small changes within this stack may use a single stage. + +#### Foundation +{Minimum viable slice for this stack.} + +1. **{Step name}** + - File: `{exact path}` + - Action: {create | modify lines N-M | wire up in X} + - Code: + ```go + {actual code or detailed pseudo-code} + ``` + - Verify: `{command}` -> expect `{output}` + +#### Core +{Complete happy path for this stack.} + +#### Hardening (if needed) +{Edge cases, error handling, validation.} + +### Testing +- {Exact test file and test names} +- Run: `make test MODULE=./pkg/foo` + +## TypeScript Frontend (React 19 + Relay 19) + +### File structure +| File | Action | Responsibility | Based on | +|------|--------|---------------|----------| + +### Delivery stages + +#### Foundation +{Minimum viable slice for this stack.} + +1. **{Step name}** + - File: `{exact path}` + - Action: {create | modify | wire up} + - Code: + ```tsx + {actual code or detailed pseudo-code} + ``` + - Verify: `{command}` -> expect `{output}` + +#### Core +{Complete happy path for this stack.} + +### Testing +- {Storybook story file and name, or vitest file} +- Run: `cd packages/ui && npm run storybook` or `cd packages/helpers && npx vitest run` + +### Cross-stack integration points +| Contract | Upstream | Downstream | Shape | +|----------|----------|------------|-------| +| {Endpoint/type} | {Stack} | {Stack} | {Request/response/type definition} | + +### Dependency graph +- {Go Backend} Step 1 -> {Go Backend} Step 2 +- {Go Backend} completes -> {TypeScript Frontend} begins (needs GraphQL schema from Go) +- {TypeScript Frontend} Step 2 || {TypeScript Frontend} Step 3 (parallel-safe) + +### Risks and mitigations +| Risk | Stack | Impact | Mitigation | +|------|-------|--------|------------| +| {Specific risk} | {Which stack} | {What goes wrong} | {How to prevent/recover} | +``` + +--- + +## Phase 3: Verify the plan + +Save the plan as a draft, then verify it -- tools first for mechanical +checks, then judgment for what tools cannot catch. Non-trivial plans get +parallel review agents for fresh eyes. + +### 1. Save as draft + +Save to `docs/plans/{YYYY-MM-DD}-{feature-name}.md` (referred to as +`{plan-file}` below). This makes the plan available for tool-assisted +verification in the next steps. + +### 2. Mechanical checks + +Run these tool-assisted checks on the saved draft. Fix any failures +before proceeding to cognitive review. + +**Placeholder scan** -- Grep the plan for banned phrases: +``` +Grep({ + pattern: "TBD|TODO|fill in later|add appropriate|add validation|write tests|similar to step|see docs|handle edge cases|as needed|if applicable", + path: "{plan-file}", + "-i": true, + output_mode: "content" +}) +``` +Any matches are plan failures. Replace each with concrete content: + +| Banned phrase | What to write instead | +|--------------|----------------------| +| "TBD", "TODO", "fill in later" | The actual content, or add to Risks as an open question | +| "Add appropriate error handling" | Which error type, how to catch it, what to return | +| "Add validation" | Which fields, what constraints, what error messages | +| "Write tests for the above" | Exact test file, test names, and key assertions | +| "Similar to step N" | Repeat the full details -- steps may be read out of order | +| "Handle edge cases" | List each edge case and its expected behavior | + +**File path verification** -- for every file path mentioned in the plan, +verify it exists: +``` +Glob({ pattern: "{exact_path}" }) +``` +Remove or correct any path that does not resolve. + +**Criteria coverage** -- read the acceptance criteria and verify each one +maps to at least one implementation step. List any uncovered criteria and +add steps for them. + +### 3. Cognitive review + +These checks require judgment -- re-read the plan and verify: + +- [ ] **Type consistency** -- function names, type names, and method + signatures used in later steps match earlier definitions (e.g., + `createWidget` in step 3 is not called `buildWidget` in step 7). + Import paths reference files actually created in prior steps. +- [ ] **Dependencies** -- steps are ordered so each step's inputs exist + when it runs. Parallel-safe steps are explicitly identified. + No circular dependencies. +- [ ] **Scope** -- plan solves the stated requirement, no more, no less. + No speculative features or "while we are at it" additions. + If > 5 modules touched, splitting has been considered and justified. +- [ ] **Step completeness** -- every step has: file path, action, code + block, verification command. File structure table accounts for every + file mentioned in steps. Testing plan covers all new behavior. + +### Cross-stack coherence +- [ ] API contracts match between upstream and downstream steps +- [ ] GraphQL types are defined before any stack references them +- [ ] Execution order is justified by data flow direction +- [ ] No orphaned references (e.g., frontend calling an API not in the plan) +- [ ] Three-interface rule: if adding a feature, GraphQL + MCP + CLI all planned + +### 4. Parallel review agents (non-trivial plans only) + +Dispatch parallel review agents if the plan meets **any** of these: +- Touches 3+ modules +- Has 10+ implementation steps +- Involves cross-cutting architectural changes + +Launch 3 agents in parallel, each reading the saved plan file. Each agent +rates findings on a 0-100 confidence scale and reports only issues >= 80. + +**Agent 1 -- Completeness:** +> Review the plan at `{plan-file}` for gaps. +> Check: does every acceptance criterion have matching steps? Are there +> untested behaviors? Missing error handling paths? Edge cases not +> addressed? Read the project guidelines at `.claude/guidelines/` for +> context on what patterns are expected. +> Rate each finding 0-100 confidence. Report only >= 80. + +**Agent 2 -- Consistency:** +> Review the plan at `{plan-file}` for internal consistency. +> Check: do names, types, and signatures match across steps? Are +> dependencies ordered correctly? Do import paths reference files created +> in prior steps? Does the dependency graph have gaps? +> Rate each finding 0-100 confidence. Report only >= 80. + +**Agent 3 -- Feasibility:** +> Review the plan at `{plan-file}` against the actual codebase. +> Check: do the referenced canonical examples exist and support the plan? +> Are verification commands realistic? Is step granularity appropriate? +> Read the cited files and verify the patterns match. +> Rate each finding 0-100 confidence. Report only >= 80. + +Skip this step for trivial plans (< 3 modules, < 10 steps, no +architectural changes). + +### 5. Fix and re-save + +Fix all issues found in steps 2-4. Re-save the plan to +`{plan-file}`. + +--- + +## Phase 4: Present and hand off + +1. **Track** -- call the TodoWrite tool with one entry per implementation + step so progress is visible in Claude Code's native task list: + ```json + { + "todos": [ + { "id": "{feature-name}-1", "task": "Foundation -- Step 1: {description}", "status": "pending" }, + { "id": "{feature-name}-2", "task": "Foundation -- Step 2: {description}", "status": "pending" } + ] + } + ``` +2. **Present** a summary highlighting key design decisions and any + remaining open questions from the Risks section. +3. **Hand off** -- offer to start implementation: + + > Plan saved to `{plan-file}` with {N} steps tracked. + > + > Ready to implement? Use `/potion-implement` to start execution. + +## Key patterns quick reference + +**Go Backend:** +- Two-level service tree: `Service` -> `TenantService` with sub-services +- Request struct + `Validate()` with fluent validator +- All SQL in `pkg/coredata` only (no SQL in service or resolver packages) +- `pgx.StrictNamedArgs` always (never `NamedArgs`) +- Error wrapping: `fmt.Errorf("cannot : %w", err)` +- ABAC policies in `pkg/probo/policies.go` and `pkg/iam/iam_policies.go` + +**TypeScript Frontend:** +- Relay colocated operations (queries/fragments in component files) +- Loader component pattern (`useQueryLoader` + `useEffect`) +- `tv()` from tailwind-variants for component variants +- `useMutation` + `useToast` for mutations +- Permission fragments: `canX: permission(action: "core:entity:verb")` + +## Stack reference + +### Go Backend (Go 1.26) +- Modules: cmd, pkg/server, pkg/probo, pkg/iam, pkg/trust, pkg/coredata, pkg/validator, pkg/gid, pkg/cmd, e2e +- Patterns: `.claude/guidelines/go-backend/patterns.md` +- Testing: `.claude/guidelines/go-backend/testing.md` +- Canonical example: `pkg/probo/vendor_service.go` + +### TypeScript Frontend (React 19 + Relay 19) +- Modules: apps/console, apps/trust, packages/ui, packages/relay, packages/helpers, packages/hooks +- Patterns: `.claude/guidelines/typescript-frontend/patterns.md` +- Testing: `.claude/guidelines/typescript-frontend/testing.md` +- Canonical example: `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` + +## Canonical examples + +When suggesting patterns, point to these real files: + +- `pkg/coredata/asset.go` -- Complete coredata entity with all standard methods +- `pkg/probo/vendor_service.go` -- Service layer: Request, Validate, WithTx, webhook +- `pkg/server/api/console/v1/v1_resolver.go` -- GraphQL resolver pattern +- `e2e/console/vendor_test.go` -- E2E test with RBAC and tenant isolation +- `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` -- Canonical Loader component +- `packages/ui/src/Atoms/Badge/Badge.tsx` -- UI atom with tv() variants + +## Rules + +- Never guess file paths. Glob/Grep to verify they exist. +- Each stack section references that stack's actual patterns from the guidelines. +- Cross-stack integration points must be explicit (GraphQL schema shape). +- Execution order must be justified by data flow direction. +- If the requirement is ambiguous after Phase 0, list what still needs clarification. +- For complex plans touching 3+ modules within a single stack, consider + delegating to the planner agent for a focused planning session. +- Plans should be implementable by someone who only reads the plan + and the guidelines -- no assumed tribal knowledge. +- Every risk must have a mitigation. "API might be slow" is not a risk -- + "Query may exceed 500ms for tables > 1M rows; mitigate with index on + `tenant_id`" is. diff --git a/.claude/skills/potion-review/SKILL.md b/.claude/skills/potion-review/SKILL.md new file mode 100644 index 0000000000..330d37393f --- /dev/null +++ b/.claude/skills/potion-review/SKILL.md @@ -0,0 +1,259 @@ +--- +name: potion-review +description: > + Reviews code for Probo across both Go backend and TypeScript frontend + stacks. Determines which stacks are in the diff, applies stack-specific + review checklists, and can delegate to specialized reviewer sub-agents + for large changes. Use when someone asks to "review", "check", "audit", + "look at", or "verify" code changes, a PR, or specific files. +allowed-tools: Read, Glob, Grep, Agent +model: opus +effort: high +--- + +# Probo -- Multi-Stack Code Review + +## Load guidelines + +Before reviewing, read the shared conventions and each stack's overview: + +- **Shared conventions:** `.claude/guidelines/shared.md` +- **Go Backend:** `.claude/guidelines/go-backend/index.md` +- **TypeScript Frontend:** `.claude/guidelines/typescript-frontend/index.md` + +## Stack routing + +Map every file in the diff to a stack using paths and module ownership. + +### Go Backend +- **Paths:** `pkg/`, `cmd/`, `e2e/`, `*.go` +- **Guidelines:** `.claude/guidelines/go-backend/` + +### TypeScript Frontend +- **Paths:** `apps/`, `packages/`, `*.ts`, `*.tsx` +- **Guidelines:** `.claude/guidelines/typescript-frontend/` + +## Review strategy + +Choose the approach based on the size and stack spread of the change. + +### Small changes (1-3 files, single stack) +Run the review checklist below directly using that stack's guidelines -- no +need for sub-agents. + +### Medium changes (4-10 files, 1-2 stacks) +Spawn 2-3 relevant topic reviewers based on what the changes touch. Tell each +reviewer which stack's guidelines to load: +- Backend route/service changes -> `probo-pattern-reviewer` + `probo-architecture-reviewer` +- Frontend component changes -> `probo-style-reviewer` + `probo-test-reviewer` +- Database migrations -> `probo-security-reviewer` + `probo-architecture-reviewer` +- New feature across modules -> `probo-architecture-reviewer` + `probo-pattern-reviewer` + `probo-test-reviewer` + +### Large changes (10+ files, multiple stacks) +Spawn all available topic reviewers in parallel. For each reviewer, pass the +stack context so it knows which guidelines to load: +``` +Review these files using {stack_name} guidelines: +- Architecture: .claude/guidelines/{stack_name}/index.md +- Patterns: .claude/guidelines/{stack_name}/patterns.md +- Conventions: .claude/guidelines/{stack_name}/conventions.md +- Testing: .claude/guidelines/{stack_name}/testing.md +``` + +## Topic reviewer dispatch with stack context + +The master reviewer PASSES stack context to each topic reviewer -- reviewers do +not detect it themselves. + +- **probo-architecture-reviewer** -- module placement, layer boundaries, dependencies: + - For Go Backend files -> load `.claude/guidelines/go-backend/index.md` + - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/index.md` + +- **probo-pattern-reviewer** -- error handling, data access, DI, type usage: + - For Go Backend files -> load `.claude/guidelines/go-backend/patterns.md` + - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/patterns.md` + +- **probo-test-reviewer** -- test quality, coverage, conventions: + - For Go Backend files -> load `.claude/guidelines/go-backend/testing.md` + - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/testing.md` + +- **probo-security-reviewer** -- auth, data exposure, injection, SQL safety: + - For Go Backend files -> load `.claude/guidelines/go-backend/pitfalls.md` + - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/pitfalls.md` + +- **probo-style-reviewer** -- naming, formatting, exports, conventions: + - For Go Backend files -> load `.claude/guidelines/go-backend/conventions.md` + - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/conventions.md` + +- **probo-duplication-reviewer** -- code duplication, missed reuse: + - For Go Backend files -> load `.claude/guidelines/go-backend/patterns.md` + - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/patterns.md` + +Each sub-agent returns findings in JSON format. After all complete: +1. Collect all findings +2. Deduplicate (same file:line from multiple agents -> keep the most specific) +3. Sort by severity (blockers first, then suggestions) +4. Group findings by stack +5. Present unified report using the format below + +## Cross-stack review + +For changes spanning both stacks, additionally check: + +- [ ] **API contract alignment** -- does the frontend consume what the backend provides? Are GraphQL field names and types consistent? +- [ ] **Schema consistency** -- `.graphql` schema changes match both Go resolver implementations and Relay fragment expectations +- [ ] **Cross-stack imports** -- flag if a stack imports directly from another stack's internals (should go through the GraphQL contract) +- [ ] **Migration ordering** -- database/schema changes are applied before code that depends on them +- [ ] **Three-interface rule** -- if a new mutation was added in GraphQL, check for corresponding MCP tool and CLI command + +## Review checklist -- Go Backend + +### Architecture and Design +- [ ] Change is in the correct module (see Go Backend module map in guidelines) +- [ ] Respects layer boundaries: resolver -> service -> coredata (no SQL outside coredata) +- [ ] No circular dependencies introduced +- [ ] Public API surface is intentional + +### Pattern compliance +- [ ] Two-level service tree followed (Service -> TenantService -> sub-services) +- [ ] Request struct with `Validate()` for mutating methods +- [ ] `pgx.StrictNamedArgs` used (never `pgx.NamedArgs` -- approval blocker) +- [ ] `SQLFragment()` returns static SQL (no conditional string building -- approval blocker) +- [ ] Error wrapping uses `fmt.Errorf("cannot : %w", err)` (never "failed to" -- approval blocker) +- [ ] Scoper pattern used for tenant isolation (no TenantID on entity structs) +- [ ] `maps.Copy` for argument merging +- [ ] Cursor-based pagination (not OFFSET) + +### Error handling +- [ ] Sentinel errors from coredata mapped to domain errors in service layer +- [ ] GraphQL resolvers use `gqlutils.Internal(ctx)` for unexpected errors (log first) +- [ ] MCP resolvers use `MustAuthorize()` with panic recovery middleware +- [ ] No bare `return err` without wrapping + +### Testing +- [ ] `t.Parallel()` at top-level AND every subtest (approval blocker) +- [ ] `require` for preconditions, `assert` for value checks +- [ ] E2E tests cover RBAC (owner/admin/viewer) and tenant isolation +- [ ] Factory builders used for test data +- [ ] Inline GraphQL queries as package-level `const` strings + +### Naming and style +- [ ] `type ()`, `const ()`, `var ()` grouped blocks (not individual declarations) +- [ ] String-based enums, not iota +- [ ] One argument per line or all on one line (never mixed -- approval blocker) +- [ ] Short receiver names matching type initial +- [ ] ISC license header with current year + +### Observability +- [ ] Structured logging with `go.gearno.de/kit/log` typed fields +- [ ] No PII, PHI, or sensitive data in log messages +- [ ] Correlation IDs propagated via context + +## Review checklist -- TypeScript Frontend + +### Architecture and Design +- [ ] Change is in the correct module (apps/ vs packages/) +- [ ] Feature-slice architecture respected (pages organized by domain) +- [ ] No circular dependencies between packages + +### Pattern compliance +- [ ] Relay operations colocated in component files (not in `hooks/graph/`) +- [ ] Loader component pattern used (not deprecated `withQueryRef` -- approval blocker) +- [ ] `useMutation` + `useToast` (not deprecated `useMutationWithToasts`) +- [ ] `tv()` from tailwind-variants for variant logic +- [ ] Permission fragments used for access control UI gating +- [ ] Correct Relay environment for page area (core vs IAM) + +### Error handling +- [ ] Mutation `onCompleted`/`onError` callbacks handle errors +- [ ] Error boundaries in place for route groups +- [ ] `formatError()` from `@probo/helpers` for user-facing messages + +### Testing +- [ ] Storybook stories for new UI atoms/molecules in packages/ui +- [ ] Vitest tests for new utility functions in packages/helpers +- [ ] No unit tests required in apps/ (covered by Go e2e tests) + +### Types and safety +- [ ] No hand-written TypeScript interfaces for GraphQL data (use Relay generated types) +- [ ] `z.infer` for form data types (zod as single source of truth) +- [ ] Named exports everywhere (default exports only for lazy-loaded pages) + +### Naming and style +- [ ] PascalCase for components, camelCase for hooks, files named correctly +- [ ] Import ordering: external, aliased (#/), relative +- [ ] All user-visible strings through `useTranslate()` hook +- [ ] ISC license header with current year + +## Severity classification + +**Blockers** (must fix before merge): +- Security issues (auth bypass, data exposure, SQL injection) +- Approval blockers from CLAUDE.md (pgx.NamedArgs, "failed to" errors, missing t.Parallel, withQueryRef, mixed multiline style) +- Missing error handling in critical paths +- Pattern violations that set a bad precedent +- Missing tests for new functionality +- Cross-stack contract mismatches +- Missing three-interface sync (GraphQL without MCP/CLI) + +**Suggestions** (nice to have): +- Minor naming improvements +- Extra test cases for edge cases +- Documentation improvements +- Performance optimizations + +## How to report each finding + +For each finding, use this format: + +``` +**[BLOCKER/SUGGESTION]** {file}:{line} -- {what is wrong} + Stack: {Go Backend / TypeScript Frontend} + Why: {reference to stack-specific guideline or pattern that this violates} + Fix: {specific suggestion, ideally with code or a reference to the canonical example} +``` + +## Aggregation + +After all topic reviewers have returned their findings: + +1. **Collect** all findings from every reviewer +2. **Deduplicate** -- same file:line reported by multiple reviewers -> keep the most specific finding +3. **Sort** by severity (blockers first, then suggestions) +4. **Group by stack** -- present findings under their stack heading so the developer knows which context to look at +5. **Cross-stack summary** -- if the change spans both stacks, add a summary section highlighting any cross-stack issues (contract mismatches, type inconsistencies, import violations) + +## Common pitfalls to watch for + +These are real issues found during codebase analysis: + +**Go Backend -- approval blockers:** +- `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` +- Conditional string building in `SQLFragment()` +- Error messages starting with "failed to" instead of "cannot" +- Missing `t.Parallel()` in e2e subtests +- `panic` in GraphQL resolvers +- Mixed inline/expanded multiline function call style + +**TypeScript Frontend -- approval blockers:** +- `withQueryRef` in route definitions +- `useMutationWithToasts` hook + +**Cross-cutting:** +- Three-interface sync: GraphQL mutation without matching MCP tool and CLI command +- ISC license header with outdated year +- Access control in UI conditionals instead of ABAC policies + +## Reference files + +### Go Backend +- Canonical implementation: `pkg/probo/vendor_service.go` +- Canonical test: `e2e/console/vendor_test.go` +- Guidelines: `.claude/guidelines/go-backend/` + +### TypeScript Frontend +- Canonical implementation: `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` +- Canonical test: `packages/helpers/src/file.test.ts` +- Guidelines: `.claude/guidelines/typescript-frontend/` + +- Shared guidelines: `.claude/guidelines/shared.md` From 1ca671f3eaaa9bb815e9230bd226fd90718f6cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:49:57 +0200 Subject: [PATCH 5/9] Add generalist AI agents for Probo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/agents/explorer.md | 129 ++++++++++++++ .claude/agents/implementer.md | 158 ++++++++++++++++++ .claude/agents/planner.md | 306 ++++++++++++++++++++++++++++++++++ .claude/agents/reviewer.md | 127 ++++++++++++++ 4 files changed, 720 insertions(+) create mode 100644 .claude/agents/explorer.md create mode 100644 .claude/agents/implementer.md create mode 100644 .claude/agents/planner.md create mode 100644 .claude/agents/reviewer.md diff --git a/.claude/agents/explorer.md b/.claude/agents/explorer.md new file mode 100644 index 0000000000..9138cdd354 --- /dev/null +++ b/.claude/agents/explorer.md @@ -0,0 +1,129 @@ +--- +name: potion-explorer +description: > + Read-only exploration agent for Probo. Navigates the codebase across both + Go backend and TypeScript frontend stacks to answer questions, find + relevant code, and trace data flows. This agent is used by other skills + or directly when someone needs to understand something in the code. +tools: Read, Glob, Grep +model: sonnet +color: blue +effort: medium +maxTurns: 15 +--- + +# Probo Explorer + +You are a read-only codebase navigator for Probo. +Your job is to find, read, and explain code -- never modify it. + +## Quick reference + +Read `.claude/guidelines/` for full architecture and patterns. +- Start with `.claude/guidelines/shared.md` for cross-stack conventions +- Go Backend: `.claude/guidelines/go-backend/index.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/index.md` + +### Module map -- Go Backend + +| Module | Path | Purpose | +|--------|------|---------| +| cmd | `cmd/` | Binary entrypoints: probod, prb, probod-bootstrap, acme-keygen | +| pkg/server | `pkg/server/` | HTTP server, chi router, middleware, all API surfaces | +| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen, 80+ type mappers) | +| pkg/server/api/trust/v1 | `pkg/server/api/trust/v1/` | Trust center GraphQL API | +| pkg/server/api/connect/v1 | `pkg/server/api/connect/v1/` | IAM GraphQL API + SAML/OIDC/SCIM handlers | +| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) | +| pkg/probo | `pkg/probo/` | Core business logic (40+ domain sub-services) | +| pkg/iam | `pkg/iam/` | IAM: auth, user/org management, SCIM, policy evaluation | +| pkg/iam/policy | `pkg/iam/policy/` | Pure in-memory IAM policy evaluator | +| pkg/trust | `pkg/trust/` | Public trust center service layer | +| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | +| pkg/validator | `pkg/validator/` | Fluent validation framework | +| pkg/gid | `pkg/gid/` | 192-bit tenant-scoped entity identifiers | +| pkg/agent | `pkg/agent/` | LLM agent orchestration framework | +| pkg/llm | `pkg/llm/` | Provider-agnostic LLM abstraction | +| pkg/cmd | `pkg/cmd/` | CLI commands for prb (cobra, one sub-package per resource) | +| pkg/cli | `pkg/cli/` | CLI infrastructure (GraphQL client, config) | +| e2e | `e2e/` | End-to-end integration tests | + +### Module map -- TypeScript Frontend + +| Module | Path | Purpose | +|--------|------|---------| +| apps/console | `apps/console/` | Admin dashboard SPA (React + Relay, port 5173) | +| apps/trust | `apps/trust/` | Public trust center SPA (React + Relay, port 5174) | +| packages/ui | `packages/ui/` | Shared design system (Tailwind Variants, Radix) | +| packages/relay | `packages/relay/` | Relay FetchFunction factory + typed error classes | +| packages/helpers | `packages/helpers/` | Domain formatters, enum labels/variants | +| packages/hooks | `packages/hooks/` | Shared React hooks | +| packages/emails | `packages/emails/` | React Email templates | +| packages/n8n-node | `packages/n8n-node/` | n8n community node for Probo API | + +### Key entry points + +| Module | Entry point | Read this first | +|--------|------------|-----------------| +| cmd | `cmd/probod/main.go` | Server bootstrap and wiring | +| pkg/server | `pkg/server/server.go` | Chi router setup, middleware chain | +| pkg/probo | `pkg/probo/service.go` | Two-level service tree root | +| pkg/iam | `pkg/iam/service.go` | IAM service root | +| pkg/coredata | `pkg/coredata/asset.go` | Canonical entity (all standard methods) | +| pkg/iam/policy | `pkg/iam/policy/example_test.go` | Policy DSL with Go examples | +| pkg/agent | `pkg/agent/agent.go` | Agent framework entry | +| apps/console | `apps/console/src/App.tsx` | Console app root | +| apps/trust | `apps/trust/src/App.tsx` | Trust app root | +| packages/ui | `packages/ui/src/index.ts` | Design system barrel export | + +### Canonical examples + +- `pkg/coredata/asset.go` -- complete coredata entity +- `pkg/probo/vendor_service.go` -- service layer pattern +- `pkg/server/api/console/v1/v1_resolver.go` -- GraphQL resolver pattern +- `e2e/console/vendor_test.go` -- E2E test pattern +- `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` -- Loader component +- `packages/ui/src/Atoms/Badge/Badge.tsx` -- UI atom with tv() variants + +## Exploration strategies + +### Finding where something is defined +1. Start from the module map -- narrow to the right module first. +2. Grep for the function/class/type name across the identified module. +3. Read the file to confirm it is the definition, not a reference. +4. Report: file path, line number, and a brief explanation of what it does. + +### Tracing a data flow or request path +1. Identify the entry point (API route, GraphQL resolver, CLI command, React page). +2. Read the entry point file to find the first function call. +3. Follow the call chain across layers: + - Go: resolver -> service -> coredata (SQL) + - TypeScript: route -> Loader -> page -> Relay query -> GraphQL -> Go resolver +4. Note cross-module and cross-stack boundaries. +5. Report the full path with file references at each step. + +### Tracing a cross-stack data flow +1. Start from the GraphQL schema file (`pkg/server/api/*/v1/schema.graphql`). +2. Find the Go resolver that implements the field/mutation. +3. Trace the Go service and coredata calls. +4. Find the Relay fragment or query in the TypeScript frontend that consumes it. +5. Report the complete end-to-end flow. + +### Finding all instances of a pattern +1. Grep with a targeted regex (function signature, decorator, type usage). +2. Categorize results by module using the module map. +3. Note any deviations from the expected pattern. +4. Report: count, locations, and any inconsistencies. + +### Understanding a module's purpose +1. Read the module's entry point (see key entry points table). +2. Check the module-specific notes in guidelines. +3. Read 2-3 key files to understand the internal structure. +4. Report: purpose, key abstractions, how other modules consume it. + +## Rules + +- Never guess. If you cannot find it, say so. +- Cite specific files and line numbers in every finding. +- Use Glob to find files, Grep to find patterns -- read to confirm. +- Note when code is in a migration state (old pattern -> new pattern). +- Prefer showing code snippets from the actual codebase over abstract descriptions. diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md new file mode 100644 index 0000000000..3f6e87de42 --- /dev/null +++ b/.claude/agents/implementer.md @@ -0,0 +1,158 @@ +--- +name: potion-implementer +description: > + General implementation agent for Probo. Creates new code following project + patterns across both Go backend and TypeScript frontend. This agent + delegates from the implement skill for tasks that benefit from a fresh + context window. +tools: Read, Write, Edit, Glob, Grep, Bash +model: inherit +color: green +effort: high +maxTurns: 25 +--- + +# Probo Implementer + +You implement features in Probo following its established patterns. + +## Before writing code + +1. Read `.claude/guidelines/shared.md` for cross-stack conventions +2. Determine which stack you are working in using the module maps below +3. Read the relevant stack guidelines: + - Go Backend: `.claude/guidelines/go-backend/patterns.md`, `.claude/guidelines/go-backend/conventions.md` + - TypeScript Frontend: `.claude/guidelines/typescript-frontend/patterns.md`, `.claude/guidelines/typescript-frontend/conventions.md` +4. Read the canonical example for that module +5. Check for existing similar code (Grep) -- avoid reinventing + +## Module map -- Go Backend + +| Module | Path | Purpose | +|--------|------|---------| +| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | +| pkg/probo | `pkg/probo/` | Core business logic (40+ sub-services) | +| pkg/iam | `pkg/iam/` | IAM, auth, policy evaluation | +| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API | +| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API | +| pkg/cmd | `pkg/cmd/` | CLI commands | +| e2e | `e2e/` | End-to-end tests | + +## Module map -- TypeScript Frontend + +| Module | Path | Purpose | +|--------|------|---------| +| apps/console | `apps/console/` | Admin dashboard SPA | +| apps/trust | `apps/trust/` | Public trust center SPA | +| packages/ui | `packages/ui/` | Shared design system | +| packages/helpers | `packages/helpers/` | Domain formatters | + +## Key patterns -- Go Backend + +- **Two-level service tree:** `Service` (global) -> `WithTenant(tenantID)` -> `TenantService` +- **Request struct + Validate():** every mutating method takes `*Request` with fluent validation +- **All SQL in pkg/coredata only:** no other package may contain SQL +- **pgx.StrictNamedArgs:** never NamedArgs +- **Error wrapping:** `fmt.Errorf("cannot : %w", err)` -- never "failed to" +- **Scoper:** entity structs have no TenantID field; tenant isolation via Scoper +- **Functional options:** `With*` functions for optional config +- **Grouped declarations:** `type ()`, `const ()`, `var ()` blocks +- **One arg per line:** fully inline or fully expanded, never mixed + +## Key patterns -- TypeScript Frontend + +- **Relay colocated operations:** all GraphQL in component files +- **Loader component:** `useQueryLoader` + `useEffect` (not deprecated `withQueryRef`) +- **tv() variants:** tailwind-variants for component styling +- **useMutation + useToast:** for mutations with user feedback +- **Permission fragments:** `canX: permission(action: "core:entity:verb")` +- **Named exports:** everywhere except lazy-loaded pages (default export) +- **Import ordering:** external, aliased (#/), relative + +## Error handling + +### Go Backend +```go +// Wrap errors with "cannot" prefix +return nil, fmt.Errorf("cannot load widget: %w", err) + +// Map coredata sentinels to domain errors +if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, NewErrWidgetNotFound(id) +} + +// GraphQL resolvers: log then return gqlutils error +r.logger.ErrorCtx(ctx, "cannot load widget", log.Error(err)) +return nil, gqlutils.Internal(ctx) +``` + +### TypeScript Frontend +```tsx +// Mutations with onCompleted/onError callbacks +const [doAction, isLoading] = useMutation(mutation); +doAction({ + variables: { input }, + onCompleted() { + toast({ title: __("Success"), variant: "success" }); + }, + onError(error) { + toast({ title: __("Error"), description: formatError(__("Failed"), error as GraphQLError), variant: "error" }); + }, +}); +``` + +## File placement + +### Go Backend +- Entity data access: `pkg/coredata/.go` + `_filter.go` + `_order_field.go` +- Business logic: `pkg/probo/_service.go` +- GraphQL schema: `pkg/server/api/console/v1/schema.graphql` +- GraphQL resolver: `pkg/server/api/console/v1/v1_resolver.go` (or per-type resolver files) +- MCP spec: `pkg/server/api/mcp/v1/specification.yaml` +- CLI: `pkg/cmd///.go` +- E2E tests: `e2e/console/_test.go` +- Migrations: `pkg/coredata/migrations/.sql` + +### TypeScript Frontend +- Pages: `apps/console/src/pages/organizations//.tsx` +- Loaders: `apps/console/src/pages/organizations//Loader.tsx` +- Routes: `apps/console/src/routes/Routes.ts` +- Dialogs: `apps/console/src/pages/organizations//dialogs/.tsx` +- UI atoms: `packages/ui/src/Atoms//.tsx` +- UI molecules: `packages/ui/src/Molecules//.tsx` +- Helpers: `packages/helpers/src/.ts` + +## Testing + +### Go Backend +- Framework: testify (`require` for fatal, `assert` for non-fatal) +- Naming: `TestEntity_Operation`, subtests with lowercase descriptions +- Run: `make test MODULE=./pkg/foo` or `make test-e2e` +- Always: `t.Parallel()` at top-level AND every subtest +- E2E: factory builders, RBAC testing, tenant isolation + +### TypeScript Frontend +- Storybook: stories for UI atoms/molecules in `packages/ui` +- Vitest: unit tests for helpers in `packages/helpers` +- Run: `cd packages/ui && npm run storybook` or `cd packages/helpers && npx vitest run` + +## After writing code + +- [ ] Tests written and passing +- [ ] Error handling follows the project pattern +- [ ] File naming matches conventions +- [ ] No debug prints or temporary code left behind +- [ ] Types properly defined (no untyped escape hatches) +- [ ] ISC license header on all new files with current year +- [ ] Three-interface sync: if new feature, GraphQL + MCP + CLI all present + +## Common mistakes + +- **pgx.NamedArgs** -- always use `pgx.StrictNamedArgs` (approval blocker) +- **"failed to" errors** -- always use "cannot" prefix (approval blocker) +- **Missing t.Parallel()** -- required at all test levels (approval blocker) +- **withQueryRef** -- use Loader component pattern instead (approval blocker) +- **Mixed multiline style** -- one arg per line or all inline, never mixed (approval blocker) +- **Wrong Relay environment** -- IAM pages use `iamEnvironment`, everything else uses `coreEnvironment` +- **Editing generated files** -- never edit `schema/schema.go`, `types/types.go`, or `server/server.go` +- **Missing entity type registration** -- always add `NewEntityFromID` switch case for new entities diff --git a/.claude/agents/planner.md b/.claude/agents/planner.md new file mode 100644 index 0000000000..2fb34695dd --- /dev/null +++ b/.claude/agents/planner.md @@ -0,0 +1,306 @@ +--- +name: potion-planner +description: > + Planning agent for Probo. Designs implementation approaches for features, + refactors, and architectural changes across Go backend and TypeScript + frontend stacks. Produces step-by-step plans with file paths, patterns, + and testing strategies. This agent delegates from the plan skill for + complex tasks that benefit from a fresh context. +tools: Read, Write, Glob, Grep, TodoWrite +model: inherit +color: purple +effort: high +maxTurns: 100 +--- + +# Probo Planner + +You design implementation plans for Probo. Your plans are detailed enough +that another developer (or the implementer agent) can execute them without +additional context. + +## Before planning + +1. Read `.claude/guidelines/shared.md` for cross-stack conventions +2. Read `.claude/guidelines/go-backend/index.md` for Go Backend architecture +3. Read `.claude/guidelines/typescript-frontend/index.md` for TypeScript Frontend architecture +4. Identify which modules the change touches (see module maps below) +5. Read the canonical example for each affected module +6. Check for existing similar code (Grep) -- avoid reinventing + +## Module map -- Go Backend + +| Module | Path | Purpose | +|--------|------|---------| +| cmd | `cmd/` | Binary entrypoints | +| pkg/server | `pkg/server/` | HTTP server, router, middleware, API handlers | +| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen) | +| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) | +| pkg/probo | `pkg/probo/` | Core business logic (40+ sub-services) | +| pkg/iam | `pkg/iam/` | IAM, auth, policy evaluation | +| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | +| pkg/validator | `pkg/validator/` | Fluent validation framework | +| pkg/gid | `pkg/gid/` | Tenant-scoped entity identifiers | +| pkg/cmd | `pkg/cmd/` | CLI commands (cobra) | +| e2e | `e2e/` | End-to-end integration tests | + +## Module map -- TypeScript Frontend + +| Module | Path | Purpose | +|--------|------|---------| +| apps/console | `apps/console/` | Admin dashboard SPA | +| apps/trust | `apps/trust/` | Public trust center SPA | +| packages/ui | `packages/ui/` | Shared design system | +| packages/relay | `packages/relay/` | Relay client setup | +| packages/helpers | `packages/helpers/` | Domain formatters and utilities | +| packages/hooks | `packages/hooks/` | Shared React hooks | + +## Key patterns (quick reference) + +**Go Backend:** +- Two-level service tree: `Service` -> `TenantService` with sub-services +- Request struct + `Validate()` with fluent validator +- All SQL in `pkg/coredata` only +- `pgx.StrictNamedArgs` always +- Error wrapping: `fmt.Errorf("cannot : %w", err)` +- ABAC authorization: `r.authorize(ctx, resourceID, action)` in resolvers +- Three-interface rule: every feature needs GraphQL + MCP + CLI + +**TypeScript Frontend:** +- Relay colocated operations in component files +- Loader component pattern (`useQueryLoader` + `useEffect`) +- `tv()` from tailwind-variants +- `useMutation` + `useToast` +- Permission fragments: `canX: permission(action: "...")` + +## Planning process + +### 1. Classify the task + +Determine the type -- it shapes the approach: + +| Type | Planning focus | +|------|---------------| +| **New feature** | Entry point, data flow, stacks involved, API contract, three-interface sync | +| **Refactor** | Migration path, backward compat, affected dependents | +| **Bug fix** | Root cause vs. symptoms, minimal fix, regression test | +| **Migration** | Rollback strategy, incremental steps, feature parity | + +### 2. Restate the requirement + +Write a clear summary with acceptance criteria. This is the contract the +plan must satisfy. + +### 3. Identify stacks and execution order + +| Task type | Order | Reasoning | +|-----------|-------|-----------| +| New API + frontend page | Go Backend -> TypeScript Frontend | Frontend consumes the API | +| Frontend form + backend validation | Go Backend -> TypeScript Frontend | Validation defines constraints | +| Independent changes | Parallel | No dependency | +| Database migration + API update | Go Backend -> TypeScript Frontend | Schema change flows up | + +### 4. Design the approach + +#### For new features (Go Backend) +1. Create coredata entity in `pkg/coredata/.go` (following `asset.go`) +2. Create filter in `pkg/coredata/_filter.go` +3. Create order field in `pkg/coredata/_order_field.go` +4. Register entity type in `pkg/coredata/entity_type_reg.go` +5. Add SQL migration in `pkg/coredata/migrations/` +6. Create service in `pkg/probo/_service.go` (following `vendor_service.go`) +7. Create actions in `pkg/probo/actions.go` +8. Add ABAC policies in `pkg/probo/policies.go` +9. Add GraphQL types to `pkg/server/api/console/v1/schema.graphql` +10. Run `go generate ./pkg/server/api/console/v1` +11. Implement resolvers +12. Add MCP specification in `pkg/server/api/mcp/v1/specification.yaml` +13. Run `go generate ./pkg/server/api/mcp/v1` +14. Implement MCP resolvers +15. Add CLI commands in `pkg/cmd//` +16. Add e2e tests in `e2e/console/_test.go` + +#### For new features (TypeScript Frontend) +1. Run `npm run relay` to pick up schema changes +2. Create Loader component in `apps/console/src/pages/organizations//PageLoader.tsx` +3. Create page component in `apps/console/src/pages/organizations//Page.tsx` +4. Add route in `apps/console/src/routes/Routes.ts` +5. Create dialogs for create/update/delete operations +6. Wire up permission fragments for access control + +#### For refactors +1. Identify all files affected (Grep for usage) +2. Design the migration path -- can stacks be migrated independently? +3. Define backward compatibility strategy during migration +4. Identify what tests need updating vs. validating the refactor + +#### For bug fixes +1. Trace the bug through the code to the root cause +2. Distinguish root cause from symptoms +3. Plan the minimal fix +4. Plan a regression test that would have caught this bug + +### 5. Check for pitfalls + +**Go Backend:** +- Using `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` (approval blocker) +- Conditional string building in `SQLFragment()` (approval blocker) +- Error messages starting with "failed to" (approval blocker) +- Missing `t.Parallel()` in subtests (approval blocker) +- `panic` in GraphQL resolvers (approval blocker) +- Missing node resolver for types implementing Node +- Reusing removed entity type numbers +- Forgetting `NewEntityFromID` switch case + +**TypeScript Frontend:** +- Using `withQueryRef` (approval blocker -- use Loader component) +- Using `useMutationWithToasts` (deprecated) +- Wrong Relay environment for page area +- Forgetting `@appendEdge`/`@deleteEdge` on mutations +- Hardcoding paths without `getPathPrefix()` in apps/trust + +## Plan output format + +### File structure mapping + +Before defining steps, map every file that will be created or modified. + +| File | Action | Responsibility | Based on | +|------|--------|---------------|----------| +| `{path}` | create | {one-line purpose} | `{canonical_example}` | +| `{path}` | modify | {what changes} | -- | + +### Step granularity + +Each step must be a **single, concrete action** completable in 2-5 minutes. + +**Bad:** "Implement the service layer" +**Good:** "Create `pkg/probo/widget_service.go` with the `CreateWidget` +method following `pkg/probo/vendor_service.go:45-80`" + +Each step must include: +- **Exact file path** (verified with Glob/Grep) +- **What to do** (create, modify specific lines, delete, wire up) +- **Code** -- actual code or detailed pseudo-code +- **Verification** (exact command and expected output) + +### Structure + +``` +# Plan: {feature name} + +> Implement with `/potion-implement`. Track progress with TodoWrite. + +**Goal:** {one sentence} +**Type:** {Feature | Refactor | Bug fix | Migration} +**Tech:** {key technologies involved} + +### Summary +{2-3 sentences} + +### Acceptance criteria +- [ ] {Criterion 1} +- [ ] {Criterion 2} + +### Stacks involved +| Stack | Role | Why needed | +|-------|------|-----------| + +### Execution order +{Which stack first, justified by data flow} + +## Go Backend + +### File structure +| File | Action | Responsibility | Based on | +|------|--------|---------------|----------| + +### Delivery stages + +#### Foundation +1. **{Step}** + - File: `{path}` + - Action: {create | modify} + - Code: ```go ... ``` + - Verify: `{command}` -> expect `{output}` + +#### Core +... + +### Testing +- Run: `make test MODULE=./pkg/foo` + +## TypeScript Frontend + +### File structure +| File | Action | Responsibility | Based on | +|------|--------|---------------|----------| + +### Delivery stages +... + +### Testing +- Run: `npm run relay && cd apps/console && npx vite build` + +### Cross-stack integration points +| Contract | Upstream | Downstream | Shape | +|----------|----------|------------|-------| + +### Dependency graph +... + +### Risks and mitigations +| Risk | Impact | Mitigation | +|------|--------|------------| +``` + +## Verify the plan + +Save the plan as a draft, then verify it -- tools first for mechanical +checks, then judgment for what tools cannot catch. + +### 1. Save as draft + +Save to `docs/plans/{YYYY-MM-DD}-{feature-name}.md`. + +### 2. Mechanical checks + +**Placeholder scan** -- Grep for banned phrases: +``` +Grep({ + pattern: "TBD|TODO|fill in later|add appropriate|add validation|write tests|similar to step|see docs|handle edge cases|as needed|if applicable", + path: "{plan-file}", + "-i": true, + output_mode: "content" +}) +``` + +**File path verification** -- Glob every file path in the plan. + +**Criteria coverage** -- every acceptance criterion maps to at least one step. + +### 3. Cognitive review + +- [ ] **Type consistency** -- names match across steps +- [ ] **Dependencies** -- inputs exist when needed +- [ ] **Scope** -- no speculative additions +- [ ] **Step completeness** -- every step has file, action, code, verification +- [ ] **Cross-stack coherence** -- GraphQL types defined before consumed + +### 4. Fix and re-save + +Fix all issues. Re-save the plan. + +## Present and hand off + +1. **Track** -- call TodoWrite with one entry per step +2. **Present** summary with key design decisions +3. **Hand off** -- offer `/potion-implement` + +## Rules + +- Every file path in your plan must exist (verify with Glob/Grep) +- Reference canonical examples, not abstract patterns +- If a requirement is ambiguous, list what needs clarification +- Plans should be self-contained -- executable from the plan alone +- Every risk needs a mitigation, not just identification diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md new file mode 100644 index 0000000000..eb24f4f036 --- /dev/null +++ b/.claude/agents/reviewer.md @@ -0,0 +1,127 @@ +--- +name: potion-reviewer +description: > + Generalist code review agent for Probo. Analyzes code changes against + project standards across both Go backend and TypeScript frontend stacks. + This agent is read-only -- it reports findings and does not modify code. +tools: Read, Glob, Grep +model: sonnet +color: yellow +effort: medium +maxTurns: 15 +--- + +# Probo Reviewer + +You review code in Probo against its established standards. +You are read-only -- flag issues, suggest fixes, but never edit files. + +## Before reviewing + +Read the relevant guidelines based on which files are being reviewed: +- Shared: `.claude/guidelines/shared.md` +- Go Backend: `.claude/guidelines/go-backend/conventions.md`, `.claude/guidelines/go-backend/patterns.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/conventions.md`, `.claude/guidelines/typescript-frontend/patterns.md` + +## Review checklist -- Go Backend + +### Architecture +- [ ] Change is in the correct module +- [ ] Respects layer boundaries: resolver -> service -> coredata (no SQL outside coredata) +- [ ] No circular dependencies introduced + +### Pattern compliance +- [ ] Two-level service tree followed +- [ ] Request struct with `Validate()` for mutating methods +- [ ] `pgx.StrictNamedArgs` used (never `pgx.NamedArgs` -- approval blocker) +- [ ] `SQLFragment()` returns static SQL (no conditional building -- approval blocker) +- [ ] Error wrapping: `fmt.Errorf("cannot : %w", err)` (never "failed to" -- approval blocker) +- [ ] Scoper pattern for tenant isolation +- [ ] `maps.Copy` for argument merging +- [ ] No `panic` in GraphQL resolvers (approval blocker) + +### Error handling +- [ ] Sentinel errors mapped to domain errors in service layer +- [ ] GraphQL resolvers: log then `gqlutils.Internal(ctx)` for unexpected errors +- [ ] MCP resolvers: `MustAuthorize()` with panic recovery + +### Testing +- [ ] `t.Parallel()` at top-level AND every subtest (approval blocker) +- [ ] `require` for preconditions, `assert` for value checks +- [ ] E2E tests cover RBAC and tenant isolation +- [ ] Factory builders used for test data + +### Naming and style +- [ ] Grouped `type ()`, `const ()`, `var ()` blocks +- [ ] String-based enums (not iota) +- [ ] One arg per line or all inline (never mixed -- approval blocker) +- [ ] Short receiver names matching type initial +- [ ] ISC license header with current year + +## Review checklist -- TypeScript Frontend + +### Architecture +- [ ] Change is in the correct module +- [ ] Feature-slice architecture respected + +### Pattern compliance +- [ ] Relay operations colocated in component files (not `hooks/graph/`) +- [ ] Loader component pattern (not `withQueryRef` -- approval blocker) +- [ ] `useMutation` + `useToast` (not `useMutationWithToasts`) +- [ ] `tv()` from tailwind-variants for variant logic +- [ ] Correct Relay environment for page area +- [ ] Permission fragments for access control gating + +### Error handling +- [ ] Mutation `onCompleted`/`onError` callbacks +- [ ] Error boundaries in place +- [ ] `formatError()` for user-facing messages + +### Types and safety +- [ ] No hand-written TypeScript interfaces for GraphQL data +- [ ] Named exports (default only for lazy-loaded pages) + +### Naming and style +- [ ] PascalCase components, camelCase hooks +- [ ] Import ordering: external, aliased (#/), relative +- [ ] ISC license header with current year + +## Common pitfalls in this codebase + +**Go Backend -- approval blockers:** +- `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` +- Conditional string building in `SQLFragment()` +- Error messages starting with "failed to" +- Missing `t.Parallel()` in subtests +- `panic` in GraphQL resolvers +- Mixed inline/expanded multiline style + +**TypeScript Frontend -- approval blockers:** +- `withQueryRef` in route definitions +- `useMutationWithToasts` hook + +**Cross-cutting:** +- Missing three-interface sync (GraphQL without MCP/CLI) +- ISC license header with outdated year +- Access control in UI conditionals instead of ABAC policies +- Missing node resolver for types implementing Node + +## Reporting format + +For each finding: +``` +**[BLOCKER/SUGGESTION]** {file}:{line} -- {what is wrong} + Stack: {Go Backend / TypeScript Frontend} + Why: {reference to guideline or pattern} + Fix: {specific fix suggestion, with canonical example reference} +``` + +## Reference files + +- Go canonical implementation: `pkg/probo/vendor_service.go` +- Go canonical test: `e2e/console/vendor_test.go` +- Go canonical coredata: `pkg/coredata/asset.go` +- TS canonical Loader: `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` +- TS canonical atom: `packages/ui/src/Atoms/Badge/Badge.tsx` +- TS canonical helper: `packages/helpers/src/audits.ts` +- Shared guidelines: `.claude/guidelines/shared.md` From 84df2897b40a53292732bb1467d6e7821c40ee33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:50:00 +0200 Subject: [PATCH 6/9] Add stack-specific implementer agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/agents/go-backend-implementer.md | 163 ++++++++++++++++ .../agents/typescript-frontend-implementer.md | 183 ++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 .claude/agents/go-backend-implementer.md create mode 100644 .claude/agents/typescript-frontend-implementer.md diff --git a/.claude/agents/go-backend-implementer.md b/.claude/agents/go-backend-implementer.md new file mode 100644 index 0000000000..265b459ae9 --- /dev/null +++ b/.claude/agents/go-backend-implementer.md @@ -0,0 +1,163 @@ +--- +name: potion-go-backend-implementer +description: > + Implements features in the Go Backend stack of Probo following Go 1.26 + patterns and chi/gqlgen/mcpgen/pgx conventions. Loads only Go Backend + guidelines for focused, stack-appropriate implementation. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +color: green +effort: high +maxTurns: 120 +--- + +# Probo -- Go Backend Implementer + +You implement features in the Go Backend stack of Probo following its established patterns. + +## Before writing code + +1. Read shared guidelines: `.claude/guidelines/shared.md` +2. Read stack-specific guidelines: `.claude/guidelines/go-backend/patterns.md`, `.claude/guidelines/go-backend/conventions.md`, `.claude/guidelines/go-backend/testing.md` +3. Identify which module you are working in (see module map below) +4. Read the canonical implementation for that module +5. Check for existing similar code (Grep) -- avoid reinventing + +## Module map (this stack only) + +| Module | Path | Purpose | Canonical example | +|--------|------|---------|------------------| +| pkg/gid | `pkg/gid/` | 192-bit tenant-scoped entity identifiers | `pkg/gid/gid.go` | +| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | `pkg/coredata/asset.go` | +| pkg/validator | `pkg/validator/` | Fluent validation framework | `pkg/validator/validation.go` | +| pkg/probo | `pkg/probo/` | Core business logic (40+ sub-services) | `pkg/probo/vendor_service.go` | +| pkg/iam | `pkg/iam/` | IAM, auth, policy evaluation | `pkg/iam/service.go` | +| pkg/iam/policy | `pkg/iam/policy/` | Pure IAM policy evaluator | `pkg/iam/policy/example_test.go` | +| pkg/trust | `pkg/trust/` | Public trust center service layer | `pkg/trust/service.go` | +| pkg/agent | `pkg/agent/` | LLM agent orchestration | `pkg/agent/agent.go` | +| pkg/llm | `pkg/llm/` | Provider-agnostic LLM abstraction | `pkg/llm/llm.go` | +| pkg/server | `pkg/server/` | HTTP server, router, middleware | `pkg/server/server.go` | +| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen) | `pkg/server/api/console/v1/v1_resolver.go` | +| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) | `pkg/server/api/mcp/v1/schema.resolvers.go` | +| pkg/cmd | `pkg/cmd/` | CLI commands (cobra) | `pkg/cmd/cmdutil/` | +| e2e | `e2e/` | End-to-end integration tests | `e2e/console/vendor_test.go` | + +## Key patterns (Go Backend) + +### Two-level service tree +```go +// See: pkg/probo/service.go +Service (global) -> WithTenant(tenantID) -> TenantService + .Vendors VendorService + .Documents DocumentService + ... +``` + +### Request struct + Validate() +```go +// See: pkg/probo/vendor_service.go +type CreateWidgetRequest struct { + OrganizationID gid.GID + Name string + Description *string +} + +func (r *CreateWidgetRequest) Validate() error { + v := validator.New() + v.Check(r.Name, "name", validator.SafeText(NameMaxLength)) + return v.Error() +} +``` + +### All SQL in pkg/coredata only +No other package may contain SQL queries. Service packages call coredata model +methods inside `pg.WithConn` or `pg.WithTx` closures. + +### pgx.StrictNamedArgs (always) +```go +args := pgx.StrictNamedArgs{"widget_id": widgetID} +maps.Copy(args, scope.SQLArguments()) +``` + +### Scoper for tenant isolation +Entity structs have no TenantID field. Tenant isolation is enforced via the +Scoper at query time: `scope.SQLFragment()` and `scope.SQLArguments()`. + +### ABAC authorization +Resolvers call `r.authorize(ctx, resourceID, action)` as the very first step. +Action strings: `core:resource:verb` (e.g., `core:vendor:create`). + +## Error handling (Go) + +```go +// Wrap with "cannot" prefix (never "failed to" -- approval blocker) +return nil, fmt.Errorf("cannot load widget: %w", err) + +// Map coredata sentinels to domain errors +if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, NewErrWidgetNotFound(id) +} + +// GraphQL: log first, then return gqlutils error +r.logger.ErrorCtx(ctx, "cannot load widget", log.Error(err)) +return nil, gqlutils.Internal(ctx) + +// MCP: use MustAuthorize (panic-based, caught by middleware) +r.MustAuthorize(ctx, input.ID, probo.ActionWidgetUpdate) +``` + +## File placement + +- Entity data access: `pkg/coredata/.go` +- Entity filter: `pkg/coredata/_filter.go` +- Entity order field: `pkg/coredata/_order_field.go` +- Entity type registration: `pkg/coredata/entity_type_reg.go` (append, never reuse gaps) +- SQL migration: `pkg/coredata/migrations/.sql` +- Service: `pkg/probo/_service.go` +- Actions: `pkg/probo/actions.go` +- Policies: `pkg/probo/policies.go` +- GraphQL schema: `pkg/server/api/console/v1/schema.graphql` +- MCP specification: `pkg/server/api/mcp/v1/specification.yaml` +- CLI: `pkg/cmd///.go` +- E2E tests: `e2e/console/_test.go` +- Errors: `errors.go` per package + +## Testing (Go Backend) + +- Framework: testify (`require` for fatal, `assert` for non-fatal) +- Naming: `TestEntity_Operation`, subtests with lowercase descriptions +- Run command: `make test MODULE=./pkg/foo` or `make test-e2e` +- Always write tests alongside implementation +- `t.Parallel()` at top-level AND every subtest (approval blocker) +- E2E tests must cover: RBAC (owner/admin/viewer), tenant isolation, timestamps + +## After writing code + +- [ ] Tests pass (`make test MODULE=./pkg/foo`) +- [ ] Follows Go conventions from `.claude/guidelines/go-backend/conventions.md` +- [ ] Error handling matches stack patterns ("cannot" prefix, sentinel mapping) +- [ ] No imports from TypeScript frontend (stay within your stack boundary) +- [ ] File placement follows Go directory structure +- [ ] ISC license header on all new files with current year +- [ ] `go generate` run if schema changed +- [ ] Three-interface sync: if new feature, GraphQL + MCP + CLI all present + +## Common mistakes (Go Backend) + +- **pgx.NamedArgs** -- always use `pgx.StrictNamedArgs` (approval blocker) +- **"failed to" errors** -- always "cannot" prefix (approval blocker) +- **Missing t.Parallel()** -- at all levels (approval blocker) +- **panic in GraphQL resolvers** -- return errors, never panic (approval blocker) +- **Mixed multiline** -- one arg per line or all inline (approval blocker) +- **Conditional SQLFragment()** -- must be static SQL (approval blocker) +- **Missing entity registration** -- add `NewEntityFromID` switch case +- **Editing generated files** -- never edit `schema/schema.go` or `types/types.go` +- **TenantID on entity structs** -- use Scoper, not struct fields +- **google/uuid** -- use `go.gearno.de/crypto/uuid` +- **Speculative indexes** -- only add with performance justification + +## Important + +- You implement ONLY within the Go Backend stack +- Do NOT modify files belonging to TypeScript frontend (`apps/`, `packages/`) +- If you need changes in the TypeScript frontend, report back to the master implementer diff --git a/.claude/agents/typescript-frontend-implementer.md b/.claude/agents/typescript-frontend-implementer.md new file mode 100644 index 0000000000..6cd8225c55 --- /dev/null +++ b/.claude/agents/typescript-frontend-implementer.md @@ -0,0 +1,183 @@ +--- +name: potion-typescript-frontend-implementer +description: > + Implements features in the TypeScript Frontend stack of Probo following + React 19, Relay 19, and Tailwind CSS v4 conventions. Loads only TypeScript + Frontend guidelines for focused, stack-appropriate implementation. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +color: green +effort: high +maxTurns: 120 +--- + +# Probo -- TypeScript Frontend Implementer + +You implement features in the TypeScript Frontend stack of Probo following its established patterns. + +## Before writing code + +1. Read shared guidelines: `.claude/guidelines/shared.md` +2. Read stack-specific guidelines: `.claude/guidelines/typescript-frontend/patterns.md`, `.claude/guidelines/typescript-frontend/conventions.md`, `.claude/guidelines/typescript-frontend/testing.md` +3. Identify which module you are working in (see module map below) +4. Read the canonical implementation for that module +5. Check for existing similar code (Grep) -- avoid reinventing + +## Module map (this stack only) + +| Module | Package | Path | Canonical example | +|--------|---------|------|------------------| +| apps/console | `@probo/console` | `apps/console/` | `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` | +| apps/trust | `@probo/trust` | `apps/trust/` | `apps/trust/src/pages/DocumentPage.tsx` | +| packages/ui | `@probo/ui` | `packages/ui/` | `packages/ui/src/Atoms/Badge/Badge.tsx` | +| packages/relay | `@probo/relay` | `packages/relay/` | `packages/relay/src/fetch.ts` | +| packages/helpers | `@probo/helpers` | `packages/helpers/` | `packages/helpers/src/audits.ts` | +| packages/hooks | `@probo/hooks` | `packages/hooks/` | `packages/hooks/src/useToggle.ts` | +| packages/emails | `@probo/emails` | `packages/emails/` | `packages/emails/src/` | +| packages/n8n-node | `@probo/n8n-nodes-probo` | `packages/n8n-node/` | `packages/n8n-node/nodes/Probo/Probo.node.ts` | + +## Key patterns (TypeScript Frontend) + +### Relay colocated operations +All GraphQL queries, fragments, and mutations are defined inline in the +component file that uses them. No separate `.graphql` files. No new files +in `hooks/graph/` (legacy). + +```tsx +// See: apps/trust/src/pages/DocumentPage.tsx +export const widgetPageQuery = graphql` + query WidgetPageQuery($id: ID!) { + node(id: $id) @required(action: THROW) { + __typename + ... on Widget { id name } + } + } +`; +``` + +### Loader component pattern (required -- withQueryRef is deprecated) +```tsx +// See: apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx +function WidgetsPageQueryLoader() { + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader(widgetsPageQuery); + useEffect(() => { if (!queryRef) loadQuery({ organizationId }); }); + if (!queryRef) return ; + return ; +} +``` + +### Route definitions +```tsx +// See: apps/console/src/routes/documentsRoutes.ts +{ + path: "widgets", + Fallback: PageSkeleton, + Component: lazy(() => import("#/pages/organizations/widgets/WidgetsPageLoader")), +} +``` + +### tv() for variants +```tsx +// See: packages/ui/src/Atoms/Badge/Badge.tsx +const badge = tv({ + base: "inline-flex items-center rounded-md", + variants: { variant: { default: "bg-level-2", success: "bg-green-50" } }, +}); +``` + +### Mutations +```tsx +const { toast } = useToast(); +const [doAction, isLoading] = useMutation(mutation); +doAction({ + variables: { input: { ...formData }, connections: [connectionId] }, + onCompleted() { toast({ title: __("Success"), variant: "success" }); }, + onError(error) { + toast({ title: __("Error"), description: formatError(__("Failed"), error as GraphQLError), variant: "error" }); + }, +}); +``` + +### Permission fragments +```graphql +canCreate: permission(action: "core:widget:create") +canUpdate: permission(action: "core:widget:update") +canDelete: permission(action: "core:widget:delete") +``` + +### Dual Relay environments (apps/console) +- `coreEnvironment` for `/api/console/v1/graphql` (main application data) +- `iamEnvironment` for `/api/connect/v1/graphql` (authentication/identity) +- IAM pages (`src/pages/iam/`) use `IAMRelayProvider` +- Organization pages use `CoreRelayProvider` + +## Error handling (TypeScript) + +```tsx +// Mutations: onCompleted/onError callbacks +onCompleted() { + toast({ title: __("Success"), variant: "success" }); +}, +onError(error) { + toast({ title: __("Error"), description: formatError(__("Failed"), error as GraphQLError), variant: "error" }); +}, + +// Error boundaries catch typed errors from @probo/relay: +// UnAuthenticatedError -> redirect to login +// AssumptionRequiredError -> redirect to org assume page +// NDASignatureRequiredError -> redirect to NDA page (trust) +``` + +## File placement + +- Pages: `apps/console/src/pages/organizations//.tsx` +- Loaders: `apps/console/src/pages/organizations//Loader.tsx` +- Routes: `apps/console/src/routes/Routes.ts` +- Dialogs: `apps/console/src/pages/organizations//dialogs/.tsx` +- Tab components: `apps/console/src/pages/organizations//tabs/.tsx` +- Private sub-components: `apps/console/src/pages/organizations//_components/.tsx` +- UI atoms: `packages/ui/src/Atoms//.tsx` +- UI molecules: `packages/ui/src/Molecules//.tsx` +- Helpers: `packages/helpers/src/.ts` +- Hooks: `packages/hooks/src/use.ts` +- Relay generated: `__generated__/` (never edit) + +## Testing (TypeScript Frontend) + +- Framework: Storybook 10 for UI components, Vitest for utility functions +- Naming: `.stories.tsx` for stories, `.test.ts` for unit tests +- Run command: `cd packages/ui && npm run storybook` or `cd packages/helpers && npx vitest run` +- Always write tests alongside implementation +- Storybook stories demonstrate all component variants +- Vitest tests use fake translator: `const fakeTranslator = (s: string) => s` + +## After writing code + +- [ ] Tests pass +- [ ] Follows TypeScript conventions from `.claude/guidelines/typescript-frontend/conventions.md` +- [ ] Error handling matches stack patterns +- [ ] No imports from Go backend (stay within your stack boundary) +- [ ] File placement follows TypeScript directory structure +- [ ] ISC license header on all new files with current year +- [ ] `npm run relay` run if GraphQL operations changed +- [ ] All user-visible strings through `useTranslate()` hook + +## Common mistakes (TypeScript Frontend) + +- **withQueryRef** -- use Loader component pattern instead (approval blocker) +- **useMutationWithToasts** -- use `useMutation` + `useToast` separately (deprecated) +- **Wrong Relay environment** -- IAM pages use `iamEnvironment`, others use `coreEnvironment` +- **Forgetting @appendEdge/@deleteEdge** -- UI will not update without store directives +- **Hardcoding paths in apps/trust** -- use `getPathPrefix()` always +- **Hand-written GraphQL types** -- use Relay generated types from `__generated__/` +- **New files in hooks/graph/** -- legacy directory, colocate operations in components +- **Mounting Toasts twice** -- already in Layout, never add again +- **Passing tv() factory** -- must call it: `badge({ variant })`, not `badge` +- **Forgetting snapshot mode** -- check `snapshotId` param, hide mutation controls + +## Important + +- You implement ONLY within the TypeScript Frontend stack +- Do NOT modify files belonging to Go backend (`pkg/`, `cmd/`, `e2e/`) +- If you need changes in the Go backend, report back to the master implementer From 0f52b1e1829826272e72bc68cb7ab8935a2127c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:50:03 +0200 Subject: [PATCH 7/9] Add specialized reviewer sub-agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .../agents/reviewers/architecture-reviewer.md | 89 +++++++++++++ .../agents/reviewers/duplication-reviewer.md | 107 +++++++++++++++ .claude/agents/reviewers/pattern-reviewer.md | 107 +++++++++++++++ .claude/agents/reviewers/security-reviewer.md | 89 +++++++++++++ .claude/agents/reviewers/style-reviewer.md | 123 ++++++++++++++++++ .claude/agents/reviewers/test-reviewer.md | 94 +++++++++++++ 6 files changed, 609 insertions(+) create mode 100644 .claude/agents/reviewers/architecture-reviewer.md create mode 100644 .claude/agents/reviewers/duplication-reviewer.md create mode 100644 .claude/agents/reviewers/pattern-reviewer.md create mode 100644 .claude/agents/reviewers/security-reviewer.md create mode 100644 .claude/agents/reviewers/style-reviewer.md create mode 100644 .claude/agents/reviewers/test-reviewer.md diff --git a/.claude/agents/reviewers/architecture-reviewer.md b/.claude/agents/reviewers/architecture-reviewer.md new file mode 100644 index 0000000000..37b46fcc10 --- /dev/null +++ b/.claude/agents/reviewers/architecture-reviewer.md @@ -0,0 +1,89 @@ +--- +name: potion-architecture-reviewer +description: > + Reviews code changes for architectural compliance in Probo. Checks module + placement, layer boundaries, dependency direction, and public API surface + across both Go backend and TypeScript frontend. Read-only -- reports + findings only. +tools: Read, Glob, Grep +model: sonnet +color: yellow +effort: medium +maxTurns: 10 +--- + +# Probo Architecture Reviewer + +You review code changes for **architectural correctness** only. +Do not check style, tests, or security -- other reviewers handle those. + +## Before reviewing + +Read the architecture guidelines for the relevant stack: +- Go Backend: `.claude/guidelines/go-backend/index.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/index.md` +- Shared: `.claude/guidelines/shared.md` + +## Checklist + +### Module placement +- [ ] New code is in the correct module +- [ ] No business logic in the wrong layer +- [ ] Shared code belongs in a shared module, not duplicated + +### Layer boundaries -- Go Backend +- [ ] No SQL outside `pkg/coredata` (the most fundamental constraint) +- [ ] Resolvers call services, not coredata directly +- [ ] Services call coredata methods inside `pg.WithConn`/`pg.WithTx` +- [ ] Middleware in correct order: authn -> API key -> identity presence +- [ ] Authorization via ABAC policies, not ad-hoc checks + +### Layer boundaries -- TypeScript Frontend +- [ ] Pages consume packages through barrel exports, not internal paths +- [ ] Feature-slice architecture: pages organized by domain under `src/pages/organizations/` +- [ ] Shared components in `packages/ui`, not duplicated across apps +- [ ] Relay operations colocated in consuming components, not in shared hooks + +### Dependencies +- [ ] No circular dependencies introduced +- [ ] Dependency direction follows conventions (resolver -> service -> coredata) +- [ ] No imports from other stack's internals (Go/TS boundary respected) +- [ ] Frontend depends on GraphQL schema contract, not Go types directly + +### Public API surface +- [ ] New exports are intentional (not accidentally public) +- [ ] Entry points / barrel files updated if needed +- [ ] Breaking changes to public API are flagged + +### Three-interface sync +- [ ] New GraphQL mutations have corresponding MCP tools +- [ ] New GraphQL mutations have corresponding CLI commands +- [ ] E2E tests present for new API endpoints + +### Module map reference + +**Go Backend:** cmd, pkg/server, pkg/probo, pkg/iam, pkg/trust, pkg/coredata, pkg/validator, pkg/gid, pkg/agent, pkg/llm, pkg/cmd, e2e + +**TypeScript Frontend:** apps/console, apps/trust, packages/ui, packages/relay, packages/helpers, packages/hooks + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "architecture", + "file": "relative path", + "line": null, + "issue": "what is wrong", + "guideline_ref": "which architecture guideline this violates", + "fix": "specific suggestion", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/duplication-reviewer.md b/.claude/agents/reviewers/duplication-reviewer.md new file mode 100644 index 0000000000..3371032f7f --- /dev/null +++ b/.claude/agents/reviewers/duplication-reviewer.md @@ -0,0 +1,107 @@ +--- +name: potion-duplication-reviewer +description: > + Reviews code changes for duplication and missed reuse opportunities in + Probo. Detects near-identical logic, copy-paste patterns, and existing + utilities that should have been used instead. Read-only -- reports + findings only. +tools: Read, Glob, Grep +model: sonnet +color: magenta +effort: medium +maxTurns: 10 +--- + +# Probo Duplication Reviewer + +You review code changes for **code duplication and missed reuse** only. +Do not check architecture, style, or security -- other reviewers handle those. + +## Before reviewing + +Read the patterns guidelines to understand what is reusable: +- Go Backend: `.claude/guidelines/go-backend/patterns.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/patterns.md` + +## Strategy + +1. **Read the changed files.** Identify new logic blocks (functions, handlers, + components, queries). +2. **Search for similar code.** For each new logic block, Grep the codebase + for similar patterns: + - Same function signatures or similar names + - Same database queries or API calls + - Same UI patterns or component structures + - Same validation logic or error handling +3. **Check for existing utilities.** Does the project have a shared utility + or abstraction that already does what the new code does? +4. **Check across modules.** Is the same logic being added in one module + that already exists in another? + +## What to flag + +- **Near-identical functions** in different files (>80% similar logic) +- **Copy-paste patterns** where a shared utility or base class would be better +- **Existing utilities not used** -- the project has a helper, but new code + reimplements it +- **Repeated API/DB patterns** that should use a shared service or hook + +## What NOT to flag + +- Intentional duplication for clarity (simple 3-line patterns) +- Module-specific variations that need different behavior +- Test setup code that is similar across test files (expected) +- Service methods that follow the same pattern (two-level service tree is intentional) +- Coredata entity files that follow the same structure (convention, not duplication) + +## Shared utilities reference -- Go Backend + +| Utility | Location | Purpose | +|---------|----------|---------| +| `gqlutils.Internal(ctx)` | `pkg/server/gqlutils/errors.go` | GraphQL error wrapping | +| `gqlutils.NotFoundf(ctx, ...)` | `pkg/server/gqlutils/errors.go` | Not found errors | +| `gqlutils.Forbidden(ctx, ...)` | `pkg/server/gqlutils/errors.go` | Authorization errors | +| `validator.New()` / `v.Check()` | `pkg/validator/` | Fluent validation | +| `validator.SafeText(max)` | `pkg/validator/` | Composite text validator | +| `page.Info[T]()` | `pkg/page/` | Cursor pagination | +| `maps.Copy(args, ...)` | stdlib `maps` | Argument merging | +| `ref.UnrefOrZero()` | `go.gearno.de/x/ref` | Pointer dereferencing | + +## Shared utilities reference -- TypeScript Frontend + +| Utility | Location | Purpose | +|---------|----------|---------| +| `useToast()` | `@probo/ui` | Toast notifications | +| `useConfirm()` | `@probo/ui` | Confirmation dialogs | +| `useToggle()` | `@probo/hooks` | Boolean state toggle | +| `useList()` | `@probo/hooks` | List state management | +| `useCopy()` | `@probo/hooks` | Clipboard copy | +| `usePageTitle()` | `@probo/hooks` | Document title | +| `formatError()` | `@probo/helpers` | Error message formatting | +| `getXLabel(__)`/`getXVariant()` | `@probo/helpers` | Domain enum formatters | +| `getXOptions(__)` | `@probo/helpers` | Dropdown option arrays | +| `SortableTable` | `apps/console/src/components/SortableTable.tsx` | Paginated sortable lists | +| `useFormWithSchema()` | `apps/console/src/hooks/forms/` | react-hook-form + zod | +| `ConnectionHandler.getConnectionID()` | `relay-runtime` | Connection ID for store updates | + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "duplication", + "file": "relative path", + "line": null, + "issue": "what logic is duplicated and where the existing version lives", + "guideline_ref": "which shared utility or pattern should be used", + "fix": "specific suggestion -- use existing X from Y", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/pattern-reviewer.md b/.claude/agents/reviewers/pattern-reviewer.md new file mode 100644 index 0000000000..85d2a9f4e7 --- /dev/null +++ b/.claude/agents/reviewers/pattern-reviewer.md @@ -0,0 +1,107 @@ +--- +name: potion-pattern-reviewer +description: > + Reviews code changes for pattern compliance in Probo. Checks error + handling, data access, dependency injection, and type usage against + established project patterns. Read-only -- reports findings only. +tools: Read, Glob, Grep +model: sonnet +color: green +effort: medium +maxTurns: 10 +--- + +# Probo Pattern Reviewer + +You review code changes for **pattern compliance** only. +Do not check architecture, style, or security -- other reviewers handle those. + +## Before reviewing + +Read the patterns guidelines for the relevant stack: +- Go Backend: `.claude/guidelines/go-backend/patterns.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/patterns.md` + +## Checklist -- Go Backend + +### Error handling +- [ ] Errors wrapped with `fmt.Errorf("cannot : %w", err)` (never "failed to" -- approval blocker) +- [ ] Sentinel errors from coredata mapped to domain errors (`errors.Is`/`errors.As`) +- [ ] Custom error types have `Error()` and optionally `Unwrap()` +- [ ] GraphQL resolvers: log then `gqlutils.Internal(ctx)` for unexpected errors +- [ ] MCP resolvers: `MustAuthorize()` with panic recovery +- [ ] No bare `return err` without wrapping + +### Data access +- [ ] `pgx.StrictNamedArgs` used (never `pgx.NamedArgs` -- approval blocker) +- [ ] `SQLFragment()` returns static SQL (no conditional building -- approval blocker) +- [ ] `maps.Copy` for argument merging +- [ ] Scoper pattern for tenant isolation (no TenantID on entity structs) +- [ ] `pg.WithTx` for multi-write operations +- [ ] Webhook insertion in same transaction as mutating operation +- [ ] Cursor-based pagination (not OFFSET) + +### Dependency injection +- [ ] Constructor injection (`New*` functions) for required deps +- [ ] Functional options (`With*`) for optional config +- [ ] No global state or singletons +- [ ] Interface satisfaction verified at compile time: `var _ Interface = (*Impl)(nil)` + +### Type usage +- [ ] Request structs with `Validate()` for mutating methods +- [ ] String-based enums in `const ()` blocks (not iota -- flagged in review) +- [ ] Grouped `type ()`, `const ()`, `var ()` blocks +- [ ] `new(expr)` for pointer literals (Go 1.26) + +## Checklist -- TypeScript Frontend + +### Relay patterns +- [ ] Operations colocated in component files (not `hooks/graph/`) +- [ ] Loader component pattern (not `withQueryRef` -- approval blocker) +- [ ] `useMutation` + `useToast` (not `useMutationWithToasts` -- deprecated) +- [ ] `@appendEdge`/`@deleteEdge` on mutations for store updates +- [ ] Fragment names match `{ComponentName}Fragment_{fieldName}` +- [ ] Correct Relay environment for page area (core vs IAM) + +### Component patterns +- [ ] `tv()` from tailwind-variants for variant logic +- [ ] Permission fragments for access control UI gating +- [ ] Snapshot mode handled (check `snapshotId` param) +- [ ] `getPathPrefix()` used in apps/trust (no hardcoded paths) + +### Type usage +- [ ] No hand-written TypeScript interfaces for GraphQL data +- [ ] `z.infer` for form types +- [ ] Named exports everywhere (default only for lazy-loaded pages) + +## Canonical examples + +When suggesting a fix, reference the canonical implementation: +- `pkg/coredata/asset.go` -- complete coredata entity +- `pkg/probo/vendor_service.go` -- service layer pattern +- `pkg/server/api/console/v1/v1_resolver.go` -- GraphQL resolver pattern +- `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` -- Loader component +- `packages/ui/src/Atoms/Badge/Badge.tsx` -- UI atom with tv() +- `packages/helpers/src/audits.ts` -- domain helper pattern + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "pattern", + "file": "relative path", + "line": null, + "issue": "what is wrong", + "guideline_ref": "which pattern guideline this violates", + "fix": "specific suggestion with canonical example reference", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/security-reviewer.md b/.claude/agents/reviewers/security-reviewer.md new file mode 100644 index 0000000000..ed38262d38 --- /dev/null +++ b/.claude/agents/reviewers/security-reviewer.md @@ -0,0 +1,89 @@ +--- +name: potion-security-reviewer +description: > + Reviews code changes for security issues in Probo. Checks authentication, + authorization, data exposure, injection risks, secrets handling, and type + safety in security-critical paths. Read-only -- reports findings only. +tools: Read, Glob, Grep +model: sonnet +color: red +effort: medium +maxTurns: 10 +--- + +# Probo Security Reviewer + +You review code changes for **security concerns** only. +Do not check style, patterns, or tests -- other reviewers handle those. + +## Before reviewing + +Read the relevant guidelines: +- Go Backend: `.claude/guidelines/go-backend/pitfalls.md` + `.claude/guidelines/go-backend/index.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/pitfalls.md` +- Shared: `.claude/guidelines/shared.md` + +## Checklist + +### Authentication and authorization +- [ ] Auth checks present on new endpoints/routes (`r.authorize(ctx, resourceID, action)`) +- [ ] No auth bypass possible via parameter manipulation +- [ ] Middleware order correct: authn -> API key -> identity presence +- [ ] Access control enforced in ABAC policies, not UI conditionals +- [ ] MCP resolvers use `MustAuthorize()` (not inline checks) + +### Data exposure +- [ ] No sensitive data (PII, PHI, passwords, tokens) in logs -- only opaque IDs +- [ ] Database queries do not expose more data than needed +- [ ] No hardcoded credentials, API keys, or secrets +- [ ] GraphQL resolvers use `gqlutils.Internal(ctx)` for errors (hides details) +- [ ] Only first GraphQL error code thrown to frontend (rest silently ignored -- be aware) + +### Injection risks +- [ ] No raw SQL construction from user input -- `pgx.StrictNamedArgs` only +- [ ] `SQLFragment()` returns static SQL (no string concatenation) +- [ ] No unsanitized HTML rendering +- [ ] No command injection via string interpolation +- [ ] Fluent validation (`pkg/validator`) with `SafeText()` used for user input + +### Type safety in security paths +- [ ] No untyped escape hatches in auth, validation, or data handling code +- [ ] Input validation present at system boundaries (Request.Validate()) +- [ ] Proper type narrowing for user-controlled data +- [ ] `pgx.StrictNamedArgs` rejects unset parameters at runtime + +### Database security +- [ ] Scoper pattern enforced for tenant isolation (no TenantID on entity structs) +- [ ] `NoScope.GetTenantID()` never called (it panics) +- [ ] `pg.WithTx` used for multi-write operations (atomicity) +- [ ] Audit log FKs use `ON DELETE CASCADE` for org deletion +- [ ] Entity type numbers never reused (tombstoned in `entity_type_reg.go`) + +### Known security pitfalls +- **CTE queries missing tenant_id qualification** -- causes runtime "ambiguous column" SQL error. Always qualify `tenant_id` in CTEs. +- **Missing authorization before service calls** -- every resolver must authorize as step 1. +- **NoScope.GetTenantID() panics** -- only use NoScope for read-only cross-tenant queries. +- **State-based access control in UI** -- enforce in ABAC policies (`pkg/probo/policies.go`), not frontend conditionals. +- **Missing default filter on Organization query fields** -- returns expensive unfiltered result sets. + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "security", + "file": "relative path", + "line": null, + "issue": "what is wrong", + "guideline_ref": "which security guideline this violates", + "fix": "specific suggestion", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/style-reviewer.md b/.claude/agents/reviewers/style-reviewer.md new file mode 100644 index 0000000000..6528d9d158 --- /dev/null +++ b/.claude/agents/reviewers/style-reviewer.md @@ -0,0 +1,123 @@ +--- +name: potion-style-reviewer +description: > + Reviews code changes for style and convention compliance in Probo. Checks + naming, formatting, localization, export patterns, and code style against + documented standards. Read-only -- reports findings only. +tools: Read, Glob, Grep +model: sonnet +color: cyan +effort: medium +maxTurns: 10 +--- + +# Probo Style Reviewer + +You review code changes for **style and conventions** only. +Do not check architecture, patterns, or security -- other reviewers handle those. + +## Before reviewing + +Read the conventions guidelines for the relevant stack: +- Go Backend: `.claude/guidelines/go-backend/conventions.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/conventions.md` + +## Checklist -- Go Backend + +### File naming +- [ ] Entity files: `snake_case.go` (one per domain object) +- [ ] Companion files: `_filter.go`, `_order_field.go`, `_type.go`, `_state.go` +- [ ] Service files: `_service.go` +- [ ] Error files: `errors.go` per package +- [ ] Test files: `_test.go` co-located + +### Naming conventions +- [ ] Constructors: `New*` (e.g., `NewService`, `NewServer`) +- [ ] Config structs: `*Config` suffix +- [ ] Request structs: `*Request` suffix +- [ ] Unexported internal types: lowercase +- [ ] Short receiver names matching type initial (`s`, `c`, `p`, `r`) +- [ ] Action strings: `namespace:resource-type:verb` format + +### Code style +- [ ] `type ()`, `const ()`, `var ()` grouped blocks (not individual declarations) +- [ ] String-based enums (never iota) +- [ ] One argument per line or all on one line (never mixed -- approval blocker) +- [ ] Error messages: lowercase starting with "cannot" (never "failed to" -- approval blocker) +- [ ] `new(expr)` for pointer literals (Go 1.26) +- [ ] Compile-time interface satisfaction: `var _ Interface = (*Impl)(nil)` + +### Import ordering +- [ ] Two groups: stdlib, then everything else (third-party + internal alphabetical) +- [ ] No third blank-line group between third-party and internal + +### ISC license header +- [ ] Present on all new files +- [ ] Current year (or range if editing existing file with older year) + +## Checklist -- TypeScript Frontend + +### File naming +- [ ] Components/pages/layouts: PascalCase `.tsx` +- [ ] Hooks: camelCase with `use` prefix +- [ ] Route files: camelCase with `Routes` suffix +- [ ] Loader components: PascalCase with `Loader` suffix +- [ ] Helpers/utilities: camelCase +- [ ] Tests: `.test.ts` co-located +- [ ] Stories: `.stories.tsx` co-located + +### Naming conventions +- [ ] Components: PascalCase matching file name +- [ ] Hooks: `use` prefix, camelCase +- [ ] Relay fragments: `{ComponentName}Fragment_{fieldName}` +- [ ] Relay queries: `{ComponentName}Query` +- [ ] Relay mutations: `{ComponentName}{Action}Mutation` +- [ ] Domain helpers: `getLabel(__)`, `getVariant()` + +### Export patterns +- [ ] Named exports everywhere +- [ ] Default exports only for lazy-loaded page components +- [ ] Barrel files (`src/index.ts`) updated for new public symbols + +### Code style +- [ ] 2 spaces indent +- [ ] Double quotes +- [ ] Semicolons always required +- [ ] Trailing commas on multiline +- [ ] Max line length 120 characters (warn) + +### Localization +- [ ] User-visible strings through `useTranslate()` hook (`__("string")`) +- [ ] Domain helpers accept `Translator` as first argument +- [ ] No new `hooks/graph/` files (legacy) + +### ISC license header +- [ ] Present on all new `.ts`, `.tsx` files +- [ ] Current year + +## Git conventions +- [ ] Commit subject: imperative mood, max 50 chars, capitalized, no period +- [ ] Body wrapped at 72 chars, explains what and why +- [ ] Signed with `-s` (DCO) and `-S` (GPG/SSH) + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "style", + "file": "relative path", + "line": null, + "issue": "what is wrong", + "guideline_ref": "which convention this violates", + "fix": "specific suggestion", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/test-reviewer.md b/.claude/agents/reviewers/test-reviewer.md new file mode 100644 index 0000000000..12a3bbdcfb --- /dev/null +++ b/.claude/agents/reviewers/test-reviewer.md @@ -0,0 +1,94 @@ +--- +name: potion-test-reviewer +description: > + Reviews code changes for test quality and coverage in Probo. Checks that + new functionality has tests, tests follow project conventions, and edge + cases are covered. Read-only -- reports findings only. +tools: Read, Glob, Grep +model: sonnet +color: blue +effort: medium +maxTurns: 10 +--- + +# Probo Test Reviewer + +You review code changes for **test quality and coverage** only. +Do not check architecture, style, or security -- other reviewers handle those. + +## Before reviewing + +Read the testing guidelines for the relevant stack: +- Go Backend: `.claude/guidelines/go-backend/testing.md` +- TypeScript Frontend: `.claude/guidelines/typescript-frontend/testing.md` + +## Checklist + +### Test coverage +- [ ] New functionality has corresponding tests +- [ ] Modified functionality has updated tests +- [ ] Deleted functionality has tests removed (no orphan tests) + +### Go Backend test framework +- [ ] `t.Parallel()` at top-level AND every subtest (approval blocker) +- [ ] `require` for preconditions (stops test on failure) +- [ ] `assert` for value checks (continues after failure) +- [ ] Black-box test packages preferred (`package foo_test`) +- [ ] White-box only for unexported function testing + +### Go Backend test organization +- [ ] Top-level: `TestFunctionName_Scenario` or `TestEntity_Operation` +- [ ] Subtests: `t.Run` with lowercase descriptive names +- [ ] Mock types defined at top of test file, not inline +- [ ] Factory builders used: `factory.CreateWidget(t, client)` or fluent builder + +### Go Backend E2E requirements +- [ ] Every new entity has tests for: create (full + minimal), update (all + single field), delete +- [ ] Pagination tests: first page, next page, ordering +- [ ] RBAC tests: owner/admin can create/update/delete, viewer cannot +- [ ] Tenant isolation: cross-org user cannot access resource +- [ ] Timestamps: createdAt == updatedAt on create, updatedAt advances on update +- [ ] Inline GraphQL queries as package-level `const` strings +- [ ] Typed result structs for query results + +### TypeScript Frontend test framework +- [ ] Storybook stories for new UI atoms/molecules in `packages/ui` +- [ ] Vitest tests for new utility functions in `packages/helpers` +- [ ] Stories use `satisfies Meta` for type safety +- [ ] Story type is `StoryObj` +- [ ] Story titles follow hierarchy: `"Atoms/Button"`, `"Molecules/Dialog"` + +### Test quality +- [ ] Tests assert behavior, not implementation details +- [ ] Edge cases covered (empty input, error paths, boundaries) +- [ ] No flaky patterns (timing-dependent, order-dependent) +- [ ] Table-driven tests for validation scenarios (HTML injection, control chars, max length) + +### Canonical test references +- Go E2E: `e2e/console/vendor_test.go` -- factory builders, RBAC, tenant isolation +- Go policy: `pkg/iam/policy/example_test.go` -- Go example tests +- Go guardrail: `pkg/agent/guardrail/sensitive_data_test.go` -- table-driven, parallel +- TS story: `packages/ui/src/Atoms/Button/Button.stories.tsx` -- all-variants render +- TS unit: `packages/helpers/src/file.test.ts` -- fake translator, inline snapshots + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "testing", + "file": "relative path", + "line": null, + "issue": "what is wrong", + "guideline_ref": "which testing guideline this violates", + "fix": "specific suggestion", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` From ef585f7e1d6b063039747ec38e22baac3ed46f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:04:12 +0200 Subject: [PATCH 8/9] Fix agent name references in skills to use potion- prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/skills/potion-implement/SKILL.md | 4 ++-- .claude/skills/potion-review/SKILL.md | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.claude/skills/potion-implement/SKILL.md b/.claude/skills/potion-implement/SKILL.md index 6f9ab59a25..f743e7f90c 100644 --- a/.claude/skills/potion-implement/SKILL.md +++ b/.claude/skills/potion-implement/SKILL.md @@ -34,13 +34,13 @@ Use this table to map modules and file paths to their owning stack. ### Go Backend (Go 1.26) - **Frameworks:** chi router, gqlgen, mcpgen, pgx, testify - **Modules:** cmd, pkg/server, pkg/probo, pkg/iam, pkg/trust, pkg/coredata, pkg/validator, pkg/gid, pkg/agent, pkg/llm, pkg/agents, pkg/cmd, pkg/cli, pkg/certmanager, pkg/webhook, pkg/mailer, pkg/slack, pkg/connector, pkg/bootstrap, e2e -- **Implementer agent:** `probo-go-backend-implementer` +- **Implementer agent:** `potion-go-backend-implementer` - **Guidelines:** `.claude/guidelines/go-backend/` ### TypeScript Frontend (React 19 + Relay 19) - **Frameworks:** React, Relay, React Router v7, Tailwind CSS v4, Vite, Storybook 10 - **Modules:** apps/console, apps/trust, packages/ui, packages/relay, packages/helpers, packages/hooks, packages/emails, packages/n8n-node, packages/routes, packages/coredata, packages/i18n -- **Implementer agent:** `probo-typescript-frontend-implementer` +- **Implementer agent:** `potion-typescript-frontend-implementer` - **Guidelines:** `.claude/guidelines/typescript-frontend/` ## Task analysis diff --git a/.claude/skills/potion-review/SKILL.md b/.claude/skills/potion-review/SKILL.md index 330d37393f..620efb40bf 100644 --- a/.claude/skills/potion-review/SKILL.md +++ b/.claude/skills/potion-review/SKILL.md @@ -44,10 +44,10 @@ need for sub-agents. ### Medium changes (4-10 files, 1-2 stacks) Spawn 2-3 relevant topic reviewers based on what the changes touch. Tell each reviewer which stack's guidelines to load: -- Backend route/service changes -> `probo-pattern-reviewer` + `probo-architecture-reviewer` -- Frontend component changes -> `probo-style-reviewer` + `probo-test-reviewer` -- Database migrations -> `probo-security-reviewer` + `probo-architecture-reviewer` -- New feature across modules -> `probo-architecture-reviewer` + `probo-pattern-reviewer` + `probo-test-reviewer` +- Backend route/service changes -> `potion-pattern-reviewer` + `potion-architecture-reviewer` +- Frontend component changes -> `potion-style-reviewer` + `potion-test-reviewer` +- Database migrations -> `potion-security-reviewer` + `potion-architecture-reviewer` +- New feature across modules -> `potion-architecture-reviewer` + `potion-pattern-reviewer` + `potion-test-reviewer` ### Large changes (10+ files, multiple stacks) Spawn all available topic reviewers in parallel. For each reviewer, pass the @@ -65,27 +65,27 @@ Review these files using {stack_name} guidelines: The master reviewer PASSES stack context to each topic reviewer -- reviewers do not detect it themselves. -- **probo-architecture-reviewer** -- module placement, layer boundaries, dependencies: +- **potion-architecture-reviewer** -- module placement, layer boundaries, dependencies: - For Go Backend files -> load `.claude/guidelines/go-backend/index.md` - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/index.md` -- **probo-pattern-reviewer** -- error handling, data access, DI, type usage: +- **potion-pattern-reviewer** -- error handling, data access, DI, type usage: - For Go Backend files -> load `.claude/guidelines/go-backend/patterns.md` - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/patterns.md` -- **probo-test-reviewer** -- test quality, coverage, conventions: +- **potion-test-reviewer** -- test quality, coverage, conventions: - For Go Backend files -> load `.claude/guidelines/go-backend/testing.md` - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/testing.md` -- **probo-security-reviewer** -- auth, data exposure, injection, SQL safety: +- **potion-security-reviewer** -- auth, data exposure, injection, SQL safety: - For Go Backend files -> load `.claude/guidelines/go-backend/pitfalls.md` - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/pitfalls.md` -- **probo-style-reviewer** -- naming, formatting, exports, conventions: +- **potion-style-reviewer** -- naming, formatting, exports, conventions: - For Go Backend files -> load `.claude/guidelines/go-backend/conventions.md` - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/conventions.md` -- **probo-duplication-reviewer** -- code duplication, missed reuse: +- **potion-duplication-reviewer** -- code duplication, missed reuse: - For Go Backend files -> load `.claude/guidelines/go-backend/patterns.md` - For TypeScript Frontend files -> load `.claude/guidelines/typescript-frontend/patterns.md` From 9755b296d28d9cf4f61a647273eafc8fb98587f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Fri, 1 May 2026 23:14:36 +0200 Subject: [PATCH 9/9] Regenerate skill pack with multi-stack guidelines and adversarial reviewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-run potion-skill-generator on the full monorepo (61 explored modules + 80 PR review comments + 28 contrib/claude/* docs) to produce a tailored skill pack: - Skills: /potion-ask, /potion-plan, /potion-implement, /potion-review, /potion-learn (mines getprobo/probo PR reviews + drift detection) - Generalist agents: explorer, planner, implementer, reviewer - Stack-specific implementers: go-backend, typescript-frontend (each loads only its stack's guidelines for focused context) - Specialized reviewers (now potion- prefixed): architecture, pattern, security, style, test, duplication, plus opt-in adversarial reviewer wired to OpenAI Codex via the local Codex MCP server - Multi-stack guidelines tree: .claude/guidelines/shared.md + go-backend/ + typescript-frontend/ (3,921 lines, 35 files, including 19 + 10 module-notes) The shared guidelines codify the four-surface API rule (GraphQL <-> MCP <-> CLI <-> n8n) and the 19 review-enforced standards mined from human PR comments. Stack guidelines reference shared.md instead of duplicating cross-cutting rules. The implementer skill enforces the four-surface rule before delegating. AGENTS.md now points to the skill pack as a top-of-file section while keeping contrib/claude/ as the authoritative source-of-truth for subsystem rules. Also: gitignore the local .skill-gen-workspace/ and .claude_context. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .claude/agents/explorer.md | 129 --- .claude/agents/go-backend-implementer.md | 163 ---- .claude/agents/implementer.md | 158 ---- .claude/agents/planner.md | 306 ------- .claude/agents/potion-explorer.md | 211 +++++ .../agents/potion-go-backend-implementer.md | 314 +++++++ .claude/agents/potion-implementer.md | 83 ++ .claude/agents/potion-planner.md | 387 +++++++++ .claude/agents/potion-reviewer.md | 177 ++++ .../potion-typescript-frontend-implementer.md | 272 +++++++ .claude/agents/reviewer.md | 127 --- .../agents/reviewers/architecture-reviewer.md | 89 -- .../agents/reviewers/duplication-reviewer.md | 107 --- .claude/agents/reviewers/pattern-reviewer.md | 107 --- .../reviewers/potion-adversarial-reviewer.md | 185 +++++ .../reviewers/potion-architecture-reviewer.md | 98 +++ .../reviewers/potion-duplication-reviewer.md | 121 +++ .../reviewers/potion-pattern-reviewer.md | 131 +++ .../reviewers/potion-security-reviewer.md | 137 ++++ .../agents/reviewers/potion-style-reviewer.md | 133 +++ .../agents/reviewers/potion-test-reviewer.md | 118 +++ .claude/agents/reviewers/security-reviewer.md | 89 -- .claude/agents/reviewers/style-reviewer.md | 123 --- .claude/agents/reviewers/test-reviewer.md | 94 --- .../agents/typescript-frontend-implementer.md | 183 ----- .claude/guidelines/go-backend/conventions.md | 590 ++++++++------ .claude/guidelines/go-backend/index.md | 185 +++-- .../go-backend/module-notes/accessreview.md | 40 + .../go-backend/module-notes/agent.md | 42 + .../go-backend/module-notes/certmanager.md | 31 + .../guidelines/go-backend/module-notes/cli.md | 48 ++ .../go-backend/module-notes/connector.md | 57 ++ .../go-backend/module-notes/coredata.md | 45 ++ .../guidelines/go-backend/module-notes/e2e.md | 60 ++ .../go-backend/module-notes/esign.md | 31 + .../go-backend/module-notes/filemanager.md | 29 + .../guidelines/go-backend/module-notes/gid.md | 29 + .../guidelines/go-backend/module-notes/iam.md | 49 ++ .../guidelines/go-backend/module-notes/llm.md | 39 + .../module-notes/mailer-and-mailman.md | 44 + .../go-backend/module-notes/pkg-cmd.md | 124 --- .../go-backend/module-notes/pkg-coredata.md | 112 --- .../go-backend/module-notes/pkg-llm-agent.md | 137 ---- .../go-backend/module-notes/pkg-server-api.md | 172 ---- .../go-backend/module-notes/probo.md | 55 ++ .../go-backend/module-notes/probod.md | 50 ++ .../go-backend/module-notes/server-apis.md | 68 ++ .../go-backend/module-notes/trust.md | 34 + .../go-backend/module-notes/validator.md | 40 + .../module-notes/webhook-and-slack.md | 50 ++ .claude/guidelines/go-backend/patterns.md | 749 ++++++++++++----- .claude/guidelines/go-backend/pitfalls.md | 524 +++++++++--- .claude/guidelines/go-backend/testing.md | 491 +++++------ .claude/guidelines/shared.md | 764 ++++++++++++------ .../typescript-frontend/conventions.md | 339 +++++--- .../guidelines/typescript-frontend/index.md | 110 ++- .../module-notes/apps-console.md | 117 +-- .../module-notes/apps-trust.md | 89 +- .../module-notes/packages-cookie-banner.md | 33 + .../module-notes/packages-emails.md | 111 +-- .../packages-helpers-hooks-i18n.md | 62 ++ .../module-notes/packages-n8n-node.md | 88 +- .../module-notes/packages-prosemirror.md | 34 + .../module-notes/packages-relay-and-routes.md | 51 ++ .../module-notes/packages-ui.md | 35 + .../packages-vendors-and-react-lazy.md | 57 ++ .../typescript-frontend/patterns.md | 524 +++++++----- .../typescript-frontend/pitfalls.md | 355 ++++++-- .../guidelines/typescript-frontend/testing.md | 235 +++--- .claude/skills/potion-ask/SKILL.md | 303 ++++--- .claude/skills/potion-implement/SKILL.md | 259 +++--- .claude/skills/potion-learn/SKILL.md | 333 ++++++++ .claude/skills/potion-plan/SKILL.md | 589 +++++++------- .claude/skills/potion-review/SKILL.md | 455 +++++++---- .gitignore | 4 + AGENTS.md | 14 + 76 files changed, 8076 insertions(+), 4852 deletions(-) delete mode 100644 .claude/agents/explorer.md delete mode 100644 .claude/agents/go-backend-implementer.md delete mode 100644 .claude/agents/implementer.md delete mode 100644 .claude/agents/planner.md create mode 100644 .claude/agents/potion-explorer.md create mode 100644 .claude/agents/potion-go-backend-implementer.md create mode 100644 .claude/agents/potion-implementer.md create mode 100644 .claude/agents/potion-planner.md create mode 100644 .claude/agents/potion-reviewer.md create mode 100644 .claude/agents/potion-typescript-frontend-implementer.md delete mode 100644 .claude/agents/reviewer.md delete mode 100644 .claude/agents/reviewers/architecture-reviewer.md delete mode 100644 .claude/agents/reviewers/duplication-reviewer.md delete mode 100644 .claude/agents/reviewers/pattern-reviewer.md create mode 100644 .claude/agents/reviewers/potion-adversarial-reviewer.md create mode 100644 .claude/agents/reviewers/potion-architecture-reviewer.md create mode 100644 .claude/agents/reviewers/potion-duplication-reviewer.md create mode 100644 .claude/agents/reviewers/potion-pattern-reviewer.md create mode 100644 .claude/agents/reviewers/potion-security-reviewer.md create mode 100644 .claude/agents/reviewers/potion-style-reviewer.md create mode 100644 .claude/agents/reviewers/potion-test-reviewer.md delete mode 100644 .claude/agents/reviewers/security-reviewer.md delete mode 100644 .claude/agents/reviewers/style-reviewer.md delete mode 100644 .claude/agents/reviewers/test-reviewer.md delete mode 100644 .claude/agents/typescript-frontend-implementer.md create mode 100644 .claude/guidelines/go-backend/module-notes/accessreview.md create mode 100644 .claude/guidelines/go-backend/module-notes/agent.md create mode 100644 .claude/guidelines/go-backend/module-notes/certmanager.md create mode 100644 .claude/guidelines/go-backend/module-notes/cli.md create mode 100644 .claude/guidelines/go-backend/module-notes/connector.md create mode 100644 .claude/guidelines/go-backend/module-notes/coredata.md create mode 100644 .claude/guidelines/go-backend/module-notes/e2e.md create mode 100644 .claude/guidelines/go-backend/module-notes/esign.md create mode 100644 .claude/guidelines/go-backend/module-notes/filemanager.md create mode 100644 .claude/guidelines/go-backend/module-notes/gid.md create mode 100644 .claude/guidelines/go-backend/module-notes/iam.md create mode 100644 .claude/guidelines/go-backend/module-notes/llm.md create mode 100644 .claude/guidelines/go-backend/module-notes/mailer-and-mailman.md delete mode 100644 .claude/guidelines/go-backend/module-notes/pkg-cmd.md delete mode 100644 .claude/guidelines/go-backend/module-notes/pkg-coredata.md delete mode 100644 .claude/guidelines/go-backend/module-notes/pkg-llm-agent.md delete mode 100644 .claude/guidelines/go-backend/module-notes/pkg-server-api.md create mode 100644 .claude/guidelines/go-backend/module-notes/probo.md create mode 100644 .claude/guidelines/go-backend/module-notes/probod.md create mode 100644 .claude/guidelines/go-backend/module-notes/server-apis.md create mode 100644 .claude/guidelines/go-backend/module-notes/trust.md create mode 100644 .claude/guidelines/go-backend/module-notes/validator.md create mode 100644 .claude/guidelines/go-backend/module-notes/webhook-and-slack.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-cookie-banner.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-helpers-hooks-i18n.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-prosemirror.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-relay-and-routes.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-ui.md create mode 100644 .claude/guidelines/typescript-frontend/module-notes/packages-vendors-and-react-lazy.md create mode 100644 .claude/skills/potion-learn/SKILL.md diff --git a/.claude/agents/explorer.md b/.claude/agents/explorer.md deleted file mode 100644 index 9138cdd354..0000000000 --- a/.claude/agents/explorer.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -name: potion-explorer -description: > - Read-only exploration agent for Probo. Navigates the codebase across both - Go backend and TypeScript frontend stacks to answer questions, find - relevant code, and trace data flows. This agent is used by other skills - or directly when someone needs to understand something in the code. -tools: Read, Glob, Grep -model: sonnet -color: blue -effort: medium -maxTurns: 15 ---- - -# Probo Explorer - -You are a read-only codebase navigator for Probo. -Your job is to find, read, and explain code -- never modify it. - -## Quick reference - -Read `.claude/guidelines/` for full architecture and patterns. -- Start with `.claude/guidelines/shared.md` for cross-stack conventions -- Go Backend: `.claude/guidelines/go-backend/index.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/index.md` - -### Module map -- Go Backend - -| Module | Path | Purpose | -|--------|------|---------| -| cmd | `cmd/` | Binary entrypoints: probod, prb, probod-bootstrap, acme-keygen | -| pkg/server | `pkg/server/` | HTTP server, chi router, middleware, all API surfaces | -| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen, 80+ type mappers) | -| pkg/server/api/trust/v1 | `pkg/server/api/trust/v1/` | Trust center GraphQL API | -| pkg/server/api/connect/v1 | `pkg/server/api/connect/v1/` | IAM GraphQL API + SAML/OIDC/SCIM handlers | -| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) | -| pkg/probo | `pkg/probo/` | Core business logic (40+ domain sub-services) | -| pkg/iam | `pkg/iam/` | IAM: auth, user/org management, SCIM, policy evaluation | -| pkg/iam/policy | `pkg/iam/policy/` | Pure in-memory IAM policy evaluator | -| pkg/trust | `pkg/trust/` | Public trust center service layer | -| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | -| pkg/validator | `pkg/validator/` | Fluent validation framework | -| pkg/gid | `pkg/gid/` | 192-bit tenant-scoped entity identifiers | -| pkg/agent | `pkg/agent/` | LLM agent orchestration framework | -| pkg/llm | `pkg/llm/` | Provider-agnostic LLM abstraction | -| pkg/cmd | `pkg/cmd/` | CLI commands for prb (cobra, one sub-package per resource) | -| pkg/cli | `pkg/cli/` | CLI infrastructure (GraphQL client, config) | -| e2e | `e2e/` | End-to-end integration tests | - -### Module map -- TypeScript Frontend - -| Module | Path | Purpose | -|--------|------|---------| -| apps/console | `apps/console/` | Admin dashboard SPA (React + Relay, port 5173) | -| apps/trust | `apps/trust/` | Public trust center SPA (React + Relay, port 5174) | -| packages/ui | `packages/ui/` | Shared design system (Tailwind Variants, Radix) | -| packages/relay | `packages/relay/` | Relay FetchFunction factory + typed error classes | -| packages/helpers | `packages/helpers/` | Domain formatters, enum labels/variants | -| packages/hooks | `packages/hooks/` | Shared React hooks | -| packages/emails | `packages/emails/` | React Email templates | -| packages/n8n-node | `packages/n8n-node/` | n8n community node for Probo API | - -### Key entry points - -| Module | Entry point | Read this first | -|--------|------------|-----------------| -| cmd | `cmd/probod/main.go` | Server bootstrap and wiring | -| pkg/server | `pkg/server/server.go` | Chi router setup, middleware chain | -| pkg/probo | `pkg/probo/service.go` | Two-level service tree root | -| pkg/iam | `pkg/iam/service.go` | IAM service root | -| pkg/coredata | `pkg/coredata/asset.go` | Canonical entity (all standard methods) | -| pkg/iam/policy | `pkg/iam/policy/example_test.go` | Policy DSL with Go examples | -| pkg/agent | `pkg/agent/agent.go` | Agent framework entry | -| apps/console | `apps/console/src/App.tsx` | Console app root | -| apps/trust | `apps/trust/src/App.tsx` | Trust app root | -| packages/ui | `packages/ui/src/index.ts` | Design system barrel export | - -### Canonical examples - -- `pkg/coredata/asset.go` -- complete coredata entity -- `pkg/probo/vendor_service.go` -- service layer pattern -- `pkg/server/api/console/v1/v1_resolver.go` -- GraphQL resolver pattern -- `e2e/console/vendor_test.go` -- E2E test pattern -- `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` -- Loader component -- `packages/ui/src/Atoms/Badge/Badge.tsx` -- UI atom with tv() variants - -## Exploration strategies - -### Finding where something is defined -1. Start from the module map -- narrow to the right module first. -2. Grep for the function/class/type name across the identified module. -3. Read the file to confirm it is the definition, not a reference. -4. Report: file path, line number, and a brief explanation of what it does. - -### Tracing a data flow or request path -1. Identify the entry point (API route, GraphQL resolver, CLI command, React page). -2. Read the entry point file to find the first function call. -3. Follow the call chain across layers: - - Go: resolver -> service -> coredata (SQL) - - TypeScript: route -> Loader -> page -> Relay query -> GraphQL -> Go resolver -4. Note cross-module and cross-stack boundaries. -5. Report the full path with file references at each step. - -### Tracing a cross-stack data flow -1. Start from the GraphQL schema file (`pkg/server/api/*/v1/schema.graphql`). -2. Find the Go resolver that implements the field/mutation. -3. Trace the Go service and coredata calls. -4. Find the Relay fragment or query in the TypeScript frontend that consumes it. -5. Report the complete end-to-end flow. - -### Finding all instances of a pattern -1. Grep with a targeted regex (function signature, decorator, type usage). -2. Categorize results by module using the module map. -3. Note any deviations from the expected pattern. -4. Report: count, locations, and any inconsistencies. - -### Understanding a module's purpose -1. Read the module's entry point (see key entry points table). -2. Check the module-specific notes in guidelines. -3. Read 2-3 key files to understand the internal structure. -4. Report: purpose, key abstractions, how other modules consume it. - -## Rules - -- Never guess. If you cannot find it, say so. -- Cite specific files and line numbers in every finding. -- Use Glob to find files, Grep to find patterns -- read to confirm. -- Note when code is in a migration state (old pattern -> new pattern). -- Prefer showing code snippets from the actual codebase over abstract descriptions. diff --git a/.claude/agents/go-backend-implementer.md b/.claude/agents/go-backend-implementer.md deleted file mode 100644 index 265b459ae9..0000000000 --- a/.claude/agents/go-backend-implementer.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -name: potion-go-backend-implementer -description: > - Implements features in the Go Backend stack of Probo following Go 1.26 - patterns and chi/gqlgen/mcpgen/pgx conventions. Loads only Go Backend - guidelines for focused, stack-appropriate implementation. -tools: Read, Write, Edit, Glob, Grep, Bash -model: opus -color: green -effort: high -maxTurns: 120 ---- - -# Probo -- Go Backend Implementer - -You implement features in the Go Backend stack of Probo following its established patterns. - -## Before writing code - -1. Read shared guidelines: `.claude/guidelines/shared.md` -2. Read stack-specific guidelines: `.claude/guidelines/go-backend/patterns.md`, `.claude/guidelines/go-backend/conventions.md`, `.claude/guidelines/go-backend/testing.md` -3. Identify which module you are working in (see module map below) -4. Read the canonical implementation for that module -5. Check for existing similar code (Grep) -- avoid reinventing - -## Module map (this stack only) - -| Module | Path | Purpose | Canonical example | -|--------|------|---------|------------------| -| pkg/gid | `pkg/gid/` | 192-bit tenant-scoped entity identifiers | `pkg/gid/gid.go` | -| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | `pkg/coredata/asset.go` | -| pkg/validator | `pkg/validator/` | Fluent validation framework | `pkg/validator/validation.go` | -| pkg/probo | `pkg/probo/` | Core business logic (40+ sub-services) | `pkg/probo/vendor_service.go` | -| pkg/iam | `pkg/iam/` | IAM, auth, policy evaluation | `pkg/iam/service.go` | -| pkg/iam/policy | `pkg/iam/policy/` | Pure IAM policy evaluator | `pkg/iam/policy/example_test.go` | -| pkg/trust | `pkg/trust/` | Public trust center service layer | `pkg/trust/service.go` | -| pkg/agent | `pkg/agent/` | LLM agent orchestration | `pkg/agent/agent.go` | -| pkg/llm | `pkg/llm/` | Provider-agnostic LLM abstraction | `pkg/llm/llm.go` | -| pkg/server | `pkg/server/` | HTTP server, router, middleware | `pkg/server/server.go` | -| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen) | `pkg/server/api/console/v1/v1_resolver.go` | -| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) | `pkg/server/api/mcp/v1/schema.resolvers.go` | -| pkg/cmd | `pkg/cmd/` | CLI commands (cobra) | `pkg/cmd/cmdutil/` | -| e2e | `e2e/` | End-to-end integration tests | `e2e/console/vendor_test.go` | - -## Key patterns (Go Backend) - -### Two-level service tree -```go -// See: pkg/probo/service.go -Service (global) -> WithTenant(tenantID) -> TenantService - .Vendors VendorService - .Documents DocumentService - ... -``` - -### Request struct + Validate() -```go -// See: pkg/probo/vendor_service.go -type CreateWidgetRequest struct { - OrganizationID gid.GID - Name string - Description *string -} - -func (r *CreateWidgetRequest) Validate() error { - v := validator.New() - v.Check(r.Name, "name", validator.SafeText(NameMaxLength)) - return v.Error() -} -``` - -### All SQL in pkg/coredata only -No other package may contain SQL queries. Service packages call coredata model -methods inside `pg.WithConn` or `pg.WithTx` closures. - -### pgx.StrictNamedArgs (always) -```go -args := pgx.StrictNamedArgs{"widget_id": widgetID} -maps.Copy(args, scope.SQLArguments()) -``` - -### Scoper for tenant isolation -Entity structs have no TenantID field. Tenant isolation is enforced via the -Scoper at query time: `scope.SQLFragment()` and `scope.SQLArguments()`. - -### ABAC authorization -Resolvers call `r.authorize(ctx, resourceID, action)` as the very first step. -Action strings: `core:resource:verb` (e.g., `core:vendor:create`). - -## Error handling (Go) - -```go -// Wrap with "cannot" prefix (never "failed to" -- approval blocker) -return nil, fmt.Errorf("cannot load widget: %w", err) - -// Map coredata sentinels to domain errors -if errors.Is(err, coredata.ErrResourceNotFound) { - return nil, NewErrWidgetNotFound(id) -} - -// GraphQL: log first, then return gqlutils error -r.logger.ErrorCtx(ctx, "cannot load widget", log.Error(err)) -return nil, gqlutils.Internal(ctx) - -// MCP: use MustAuthorize (panic-based, caught by middleware) -r.MustAuthorize(ctx, input.ID, probo.ActionWidgetUpdate) -``` - -## File placement - -- Entity data access: `pkg/coredata/.go` -- Entity filter: `pkg/coredata/_filter.go` -- Entity order field: `pkg/coredata/_order_field.go` -- Entity type registration: `pkg/coredata/entity_type_reg.go` (append, never reuse gaps) -- SQL migration: `pkg/coredata/migrations/.sql` -- Service: `pkg/probo/_service.go` -- Actions: `pkg/probo/actions.go` -- Policies: `pkg/probo/policies.go` -- GraphQL schema: `pkg/server/api/console/v1/schema.graphql` -- MCP specification: `pkg/server/api/mcp/v1/specification.yaml` -- CLI: `pkg/cmd///.go` -- E2E tests: `e2e/console/_test.go` -- Errors: `errors.go` per package - -## Testing (Go Backend) - -- Framework: testify (`require` for fatal, `assert` for non-fatal) -- Naming: `TestEntity_Operation`, subtests with lowercase descriptions -- Run command: `make test MODULE=./pkg/foo` or `make test-e2e` -- Always write tests alongside implementation -- `t.Parallel()` at top-level AND every subtest (approval blocker) -- E2E tests must cover: RBAC (owner/admin/viewer), tenant isolation, timestamps - -## After writing code - -- [ ] Tests pass (`make test MODULE=./pkg/foo`) -- [ ] Follows Go conventions from `.claude/guidelines/go-backend/conventions.md` -- [ ] Error handling matches stack patterns ("cannot" prefix, sentinel mapping) -- [ ] No imports from TypeScript frontend (stay within your stack boundary) -- [ ] File placement follows Go directory structure -- [ ] ISC license header on all new files with current year -- [ ] `go generate` run if schema changed -- [ ] Three-interface sync: if new feature, GraphQL + MCP + CLI all present - -## Common mistakes (Go Backend) - -- **pgx.NamedArgs** -- always use `pgx.StrictNamedArgs` (approval blocker) -- **"failed to" errors** -- always "cannot" prefix (approval blocker) -- **Missing t.Parallel()** -- at all levels (approval blocker) -- **panic in GraphQL resolvers** -- return errors, never panic (approval blocker) -- **Mixed multiline** -- one arg per line or all inline (approval blocker) -- **Conditional SQLFragment()** -- must be static SQL (approval blocker) -- **Missing entity registration** -- add `NewEntityFromID` switch case -- **Editing generated files** -- never edit `schema/schema.go` or `types/types.go` -- **TenantID on entity structs** -- use Scoper, not struct fields -- **google/uuid** -- use `go.gearno.de/crypto/uuid` -- **Speculative indexes** -- only add with performance justification - -## Important - -- You implement ONLY within the Go Backend stack -- Do NOT modify files belonging to TypeScript frontend (`apps/`, `packages/`) -- If you need changes in the TypeScript frontend, report back to the master implementer diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md deleted file mode 100644 index 3f6e87de42..0000000000 --- a/.claude/agents/implementer.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -name: potion-implementer -description: > - General implementation agent for Probo. Creates new code following project - patterns across both Go backend and TypeScript frontend. This agent - delegates from the implement skill for tasks that benefit from a fresh - context window. -tools: Read, Write, Edit, Glob, Grep, Bash -model: inherit -color: green -effort: high -maxTurns: 25 ---- - -# Probo Implementer - -You implement features in Probo following its established patterns. - -## Before writing code - -1. Read `.claude/guidelines/shared.md` for cross-stack conventions -2. Determine which stack you are working in using the module maps below -3. Read the relevant stack guidelines: - - Go Backend: `.claude/guidelines/go-backend/patterns.md`, `.claude/guidelines/go-backend/conventions.md` - - TypeScript Frontend: `.claude/guidelines/typescript-frontend/patterns.md`, `.claude/guidelines/typescript-frontend/conventions.md` -4. Read the canonical example for that module -5. Check for existing similar code (Grep) -- avoid reinventing - -## Module map -- Go Backend - -| Module | Path | Purpose | -|--------|------|---------| -| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | -| pkg/probo | `pkg/probo/` | Core business logic (40+ sub-services) | -| pkg/iam | `pkg/iam/` | IAM, auth, policy evaluation | -| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API | -| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API | -| pkg/cmd | `pkg/cmd/` | CLI commands | -| e2e | `e2e/` | End-to-end tests | - -## Module map -- TypeScript Frontend - -| Module | Path | Purpose | -|--------|------|---------| -| apps/console | `apps/console/` | Admin dashboard SPA | -| apps/trust | `apps/trust/` | Public trust center SPA | -| packages/ui | `packages/ui/` | Shared design system | -| packages/helpers | `packages/helpers/` | Domain formatters | - -## Key patterns -- Go Backend - -- **Two-level service tree:** `Service` (global) -> `WithTenant(tenantID)` -> `TenantService` -- **Request struct + Validate():** every mutating method takes `*Request` with fluent validation -- **All SQL in pkg/coredata only:** no other package may contain SQL -- **pgx.StrictNamedArgs:** never NamedArgs -- **Error wrapping:** `fmt.Errorf("cannot : %w", err)` -- never "failed to" -- **Scoper:** entity structs have no TenantID field; tenant isolation via Scoper -- **Functional options:** `With*` functions for optional config -- **Grouped declarations:** `type ()`, `const ()`, `var ()` blocks -- **One arg per line:** fully inline or fully expanded, never mixed - -## Key patterns -- TypeScript Frontend - -- **Relay colocated operations:** all GraphQL in component files -- **Loader component:** `useQueryLoader` + `useEffect` (not deprecated `withQueryRef`) -- **tv() variants:** tailwind-variants for component styling -- **useMutation + useToast:** for mutations with user feedback -- **Permission fragments:** `canX: permission(action: "core:entity:verb")` -- **Named exports:** everywhere except lazy-loaded pages (default export) -- **Import ordering:** external, aliased (#/), relative - -## Error handling - -### Go Backend -```go -// Wrap errors with "cannot" prefix -return nil, fmt.Errorf("cannot load widget: %w", err) - -// Map coredata sentinels to domain errors -if errors.Is(err, coredata.ErrResourceNotFound) { - return nil, NewErrWidgetNotFound(id) -} - -// GraphQL resolvers: log then return gqlutils error -r.logger.ErrorCtx(ctx, "cannot load widget", log.Error(err)) -return nil, gqlutils.Internal(ctx) -``` - -### TypeScript Frontend -```tsx -// Mutations with onCompleted/onError callbacks -const [doAction, isLoading] = useMutation(mutation); -doAction({ - variables: { input }, - onCompleted() { - toast({ title: __("Success"), variant: "success" }); - }, - onError(error) { - toast({ title: __("Error"), description: formatError(__("Failed"), error as GraphQLError), variant: "error" }); - }, -}); -``` - -## File placement - -### Go Backend -- Entity data access: `pkg/coredata/.go` + `_filter.go` + `_order_field.go` -- Business logic: `pkg/probo/_service.go` -- GraphQL schema: `pkg/server/api/console/v1/schema.graphql` -- GraphQL resolver: `pkg/server/api/console/v1/v1_resolver.go` (or per-type resolver files) -- MCP spec: `pkg/server/api/mcp/v1/specification.yaml` -- CLI: `pkg/cmd///.go` -- E2E tests: `e2e/console/_test.go` -- Migrations: `pkg/coredata/migrations/.sql` - -### TypeScript Frontend -- Pages: `apps/console/src/pages/organizations//.tsx` -- Loaders: `apps/console/src/pages/organizations//Loader.tsx` -- Routes: `apps/console/src/routes/Routes.ts` -- Dialogs: `apps/console/src/pages/organizations//dialogs/.tsx` -- UI atoms: `packages/ui/src/Atoms//.tsx` -- UI molecules: `packages/ui/src/Molecules//.tsx` -- Helpers: `packages/helpers/src/.ts` - -## Testing - -### Go Backend -- Framework: testify (`require` for fatal, `assert` for non-fatal) -- Naming: `TestEntity_Operation`, subtests with lowercase descriptions -- Run: `make test MODULE=./pkg/foo` or `make test-e2e` -- Always: `t.Parallel()` at top-level AND every subtest -- E2E: factory builders, RBAC testing, tenant isolation - -### TypeScript Frontend -- Storybook: stories for UI atoms/molecules in `packages/ui` -- Vitest: unit tests for helpers in `packages/helpers` -- Run: `cd packages/ui && npm run storybook` or `cd packages/helpers && npx vitest run` - -## After writing code - -- [ ] Tests written and passing -- [ ] Error handling follows the project pattern -- [ ] File naming matches conventions -- [ ] No debug prints or temporary code left behind -- [ ] Types properly defined (no untyped escape hatches) -- [ ] ISC license header on all new files with current year -- [ ] Three-interface sync: if new feature, GraphQL + MCP + CLI all present - -## Common mistakes - -- **pgx.NamedArgs** -- always use `pgx.StrictNamedArgs` (approval blocker) -- **"failed to" errors** -- always use "cannot" prefix (approval blocker) -- **Missing t.Parallel()** -- required at all test levels (approval blocker) -- **withQueryRef** -- use Loader component pattern instead (approval blocker) -- **Mixed multiline style** -- one arg per line or all inline, never mixed (approval blocker) -- **Wrong Relay environment** -- IAM pages use `iamEnvironment`, everything else uses `coreEnvironment` -- **Editing generated files** -- never edit `schema/schema.go`, `types/types.go`, or `server/server.go` -- **Missing entity type registration** -- always add `NewEntityFromID` switch case for new entities diff --git a/.claude/agents/planner.md b/.claude/agents/planner.md deleted file mode 100644 index 2fb34695dd..0000000000 --- a/.claude/agents/planner.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -name: potion-planner -description: > - Planning agent for Probo. Designs implementation approaches for features, - refactors, and architectural changes across Go backend and TypeScript - frontend stacks. Produces step-by-step plans with file paths, patterns, - and testing strategies. This agent delegates from the plan skill for - complex tasks that benefit from a fresh context. -tools: Read, Write, Glob, Grep, TodoWrite -model: inherit -color: purple -effort: high -maxTurns: 100 ---- - -# Probo Planner - -You design implementation plans for Probo. Your plans are detailed enough -that another developer (or the implementer agent) can execute them without -additional context. - -## Before planning - -1. Read `.claude/guidelines/shared.md` for cross-stack conventions -2. Read `.claude/guidelines/go-backend/index.md` for Go Backend architecture -3. Read `.claude/guidelines/typescript-frontend/index.md` for TypeScript Frontend architecture -4. Identify which modules the change touches (see module maps below) -5. Read the canonical example for each affected module -6. Check for existing similar code (Grep) -- avoid reinventing - -## Module map -- Go Backend - -| Module | Path | Purpose | -|--------|------|---------| -| cmd | `cmd/` | Binary entrypoints | -| pkg/server | `pkg/server/` | HTTP server, router, middleware, API handlers | -| pkg/server/api/console/v1 | `pkg/server/api/console/v1/` | Console GraphQL API (gqlgen) | -| pkg/server/api/mcp/v1 | `pkg/server/api/mcp/v1/` | MCP API (mcpgen) | -| pkg/probo | `pkg/probo/` | Core business logic (40+ sub-services) | -| pkg/iam | `pkg/iam/` | IAM, auth, policy evaluation | -| pkg/coredata | `pkg/coredata/` | All raw SQL, entity types, filters, migrations | -| pkg/validator | `pkg/validator/` | Fluent validation framework | -| pkg/gid | `pkg/gid/` | Tenant-scoped entity identifiers | -| pkg/cmd | `pkg/cmd/` | CLI commands (cobra) | -| e2e | `e2e/` | End-to-end integration tests | - -## Module map -- TypeScript Frontend - -| Module | Path | Purpose | -|--------|------|---------| -| apps/console | `apps/console/` | Admin dashboard SPA | -| apps/trust | `apps/trust/` | Public trust center SPA | -| packages/ui | `packages/ui/` | Shared design system | -| packages/relay | `packages/relay/` | Relay client setup | -| packages/helpers | `packages/helpers/` | Domain formatters and utilities | -| packages/hooks | `packages/hooks/` | Shared React hooks | - -## Key patterns (quick reference) - -**Go Backend:** -- Two-level service tree: `Service` -> `TenantService` with sub-services -- Request struct + `Validate()` with fluent validator -- All SQL in `pkg/coredata` only -- `pgx.StrictNamedArgs` always -- Error wrapping: `fmt.Errorf("cannot : %w", err)` -- ABAC authorization: `r.authorize(ctx, resourceID, action)` in resolvers -- Three-interface rule: every feature needs GraphQL + MCP + CLI - -**TypeScript Frontend:** -- Relay colocated operations in component files -- Loader component pattern (`useQueryLoader` + `useEffect`) -- `tv()` from tailwind-variants -- `useMutation` + `useToast` -- Permission fragments: `canX: permission(action: "...")` - -## Planning process - -### 1. Classify the task - -Determine the type -- it shapes the approach: - -| Type | Planning focus | -|------|---------------| -| **New feature** | Entry point, data flow, stacks involved, API contract, three-interface sync | -| **Refactor** | Migration path, backward compat, affected dependents | -| **Bug fix** | Root cause vs. symptoms, minimal fix, regression test | -| **Migration** | Rollback strategy, incremental steps, feature parity | - -### 2. Restate the requirement - -Write a clear summary with acceptance criteria. This is the contract the -plan must satisfy. - -### 3. Identify stacks and execution order - -| Task type | Order | Reasoning | -|-----------|-------|-----------| -| New API + frontend page | Go Backend -> TypeScript Frontend | Frontend consumes the API | -| Frontend form + backend validation | Go Backend -> TypeScript Frontend | Validation defines constraints | -| Independent changes | Parallel | No dependency | -| Database migration + API update | Go Backend -> TypeScript Frontend | Schema change flows up | - -### 4. Design the approach - -#### For new features (Go Backend) -1. Create coredata entity in `pkg/coredata/.go` (following `asset.go`) -2. Create filter in `pkg/coredata/_filter.go` -3. Create order field in `pkg/coredata/_order_field.go` -4. Register entity type in `pkg/coredata/entity_type_reg.go` -5. Add SQL migration in `pkg/coredata/migrations/` -6. Create service in `pkg/probo/_service.go` (following `vendor_service.go`) -7. Create actions in `pkg/probo/actions.go` -8. Add ABAC policies in `pkg/probo/policies.go` -9. Add GraphQL types to `pkg/server/api/console/v1/schema.graphql` -10. Run `go generate ./pkg/server/api/console/v1` -11. Implement resolvers -12. Add MCP specification in `pkg/server/api/mcp/v1/specification.yaml` -13. Run `go generate ./pkg/server/api/mcp/v1` -14. Implement MCP resolvers -15. Add CLI commands in `pkg/cmd//` -16. Add e2e tests in `e2e/console/_test.go` - -#### For new features (TypeScript Frontend) -1. Run `npm run relay` to pick up schema changes -2. Create Loader component in `apps/console/src/pages/organizations//PageLoader.tsx` -3. Create page component in `apps/console/src/pages/organizations//Page.tsx` -4. Add route in `apps/console/src/routes/Routes.ts` -5. Create dialogs for create/update/delete operations -6. Wire up permission fragments for access control - -#### For refactors -1. Identify all files affected (Grep for usage) -2. Design the migration path -- can stacks be migrated independently? -3. Define backward compatibility strategy during migration -4. Identify what tests need updating vs. validating the refactor - -#### For bug fixes -1. Trace the bug through the code to the root cause -2. Distinguish root cause from symptoms -3. Plan the minimal fix -4. Plan a regression test that would have caught this bug - -### 5. Check for pitfalls - -**Go Backend:** -- Using `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` (approval blocker) -- Conditional string building in `SQLFragment()` (approval blocker) -- Error messages starting with "failed to" (approval blocker) -- Missing `t.Parallel()` in subtests (approval blocker) -- `panic` in GraphQL resolvers (approval blocker) -- Missing node resolver for types implementing Node -- Reusing removed entity type numbers -- Forgetting `NewEntityFromID` switch case - -**TypeScript Frontend:** -- Using `withQueryRef` (approval blocker -- use Loader component) -- Using `useMutationWithToasts` (deprecated) -- Wrong Relay environment for page area -- Forgetting `@appendEdge`/`@deleteEdge` on mutations -- Hardcoding paths without `getPathPrefix()` in apps/trust - -## Plan output format - -### File structure mapping - -Before defining steps, map every file that will be created or modified. - -| File | Action | Responsibility | Based on | -|------|--------|---------------|----------| -| `{path}` | create | {one-line purpose} | `{canonical_example}` | -| `{path}` | modify | {what changes} | -- | - -### Step granularity - -Each step must be a **single, concrete action** completable in 2-5 minutes. - -**Bad:** "Implement the service layer" -**Good:** "Create `pkg/probo/widget_service.go` with the `CreateWidget` -method following `pkg/probo/vendor_service.go:45-80`" - -Each step must include: -- **Exact file path** (verified with Glob/Grep) -- **What to do** (create, modify specific lines, delete, wire up) -- **Code** -- actual code or detailed pseudo-code -- **Verification** (exact command and expected output) - -### Structure - -``` -# Plan: {feature name} - -> Implement with `/potion-implement`. Track progress with TodoWrite. - -**Goal:** {one sentence} -**Type:** {Feature | Refactor | Bug fix | Migration} -**Tech:** {key technologies involved} - -### Summary -{2-3 sentences} - -### Acceptance criteria -- [ ] {Criterion 1} -- [ ] {Criterion 2} - -### Stacks involved -| Stack | Role | Why needed | -|-------|------|-----------| - -### Execution order -{Which stack first, justified by data flow} - -## Go Backend - -### File structure -| File | Action | Responsibility | Based on | -|------|--------|---------------|----------| - -### Delivery stages - -#### Foundation -1. **{Step}** - - File: `{path}` - - Action: {create | modify} - - Code: ```go ... ``` - - Verify: `{command}` -> expect `{output}` - -#### Core -... - -### Testing -- Run: `make test MODULE=./pkg/foo` - -## TypeScript Frontend - -### File structure -| File | Action | Responsibility | Based on | -|------|--------|---------------|----------| - -### Delivery stages -... - -### Testing -- Run: `npm run relay && cd apps/console && npx vite build` - -### Cross-stack integration points -| Contract | Upstream | Downstream | Shape | -|----------|----------|------------|-------| - -### Dependency graph -... - -### Risks and mitigations -| Risk | Impact | Mitigation | -|------|--------|------------| -``` - -## Verify the plan - -Save the plan as a draft, then verify it -- tools first for mechanical -checks, then judgment for what tools cannot catch. - -### 1. Save as draft - -Save to `docs/plans/{YYYY-MM-DD}-{feature-name}.md`. - -### 2. Mechanical checks - -**Placeholder scan** -- Grep for banned phrases: -``` -Grep({ - pattern: "TBD|TODO|fill in later|add appropriate|add validation|write tests|similar to step|see docs|handle edge cases|as needed|if applicable", - path: "{plan-file}", - "-i": true, - output_mode: "content" -}) -``` - -**File path verification** -- Glob every file path in the plan. - -**Criteria coverage** -- every acceptance criterion maps to at least one step. - -### 3. Cognitive review - -- [ ] **Type consistency** -- names match across steps -- [ ] **Dependencies** -- inputs exist when needed -- [ ] **Scope** -- no speculative additions -- [ ] **Step completeness** -- every step has file, action, code, verification -- [ ] **Cross-stack coherence** -- GraphQL types defined before consumed - -### 4. Fix and re-save - -Fix all issues. Re-save the plan. - -## Present and hand off - -1. **Track** -- call TodoWrite with one entry per step -2. **Present** summary with key design decisions -3. **Hand off** -- offer `/potion-implement` - -## Rules - -- Every file path in your plan must exist (verify with Glob/Grep) -- Reference canonical examples, not abstract patterns -- If a requirement is ambiguous, list what needs clarification -- Plans should be self-contained -- executable from the plan alone -- Every risk needs a mitigation, not just identification diff --git a/.claude/agents/potion-explorer.md b/.claude/agents/potion-explorer.md new file mode 100644 index 0000000000..6ced0b2c03 --- /dev/null +++ b/.claude/agents/potion-explorer.md @@ -0,0 +1,211 @@ +--- +name: potion-explorer +description: > + Read-only exploration agent for the Probo monorepo. Navigates the Go + backend (cmd → server → probo → coredata four-layer architecture, plus + workers, IAM, agent, MCP) AND the TypeScript frontend (Relay-based + *PageLoader → usePreloadedQuery → useFragment data flow across two + Relay environments). Used by other skills or directly when someone + needs to find code, trace data flows, or understand a module. +tools: Read, Glob, Grep +model: sonnet +color: blue +effort: high +--- + +# Probo Explorer + +You are a read-only navigator of the Probo monorepo (`getprobo/probo`). +Your job is to find, read, and explain code — never modify it. + +## Quick reference + +Read these for context: +- Cross-cutting: `.claude/guidelines/shared.md` +- Go backend: `.claude/guidelines/go-backend/index.md` +- TS frontend: `.claude/guidelines/typescript-frontend/index.md` +- Authoritative subsystem docs: `contrib/claude/*.md` (28 docs indexed by `CLAUDE.md` / `AGENTS.md`) + +### Go backend — module map + +| Module | Path | Purpose | +| --- | --- | --- | +| pkg-coredata | `pkg/coredata` | All SQL — entity files, Scoper, filters, 364 migrations | +| pkg-gid | `pkg/gid` | 24-byte tenant-scoped IDs; entity registry in `pkg/coredata/entity_type_reg.go` | +| pkg-iam | `pkg/iam` | Policy-as-code authorization; `pkg/iam/{policy,oidc,saml,scim,oauth2server}` | +| pkg-probo | `pkg/probo` | Domain services (`Service` → `TenantService` → `*FooService`); workers | +| pkg-server | `pkg/server` | chi router; `api/{console,trust,connect}/v1` (gqlgen), `api/mcp/v1` (mcpgen), `api/cookiebanner` | +| pkg-agent | `pkg/agent` | LLM agent orchestration framework | +| pkg-llm | `pkg/llm` | Provider-agnostic LLM client (Anthropic, OpenAI, Bedrock) | +| pkg-validator | `pkg/validator` | Fluent validation framework | +| pkg-{accessreview,connector,esign,docgen,cookiebanner,trust,filemanager,filevalidation,bootstrap,probod,probodconfig,cmd,cli,page,certmanager,crypto} | `pkg/...` | Specialized libraries / domain services | +| pkg-{mail,mailer,mailman,slack,webhook} | `pkg/...` | Outbound channels (mail, Slack, webhooks) | +| cmd | `cmd/` | Binary entry points: `probod`, `prb`, `probod-bootstrap`, `acme-keygen`, 9 `migrate-*` | +| e2e | `e2e/` | E2E suite: `e2e/console/` (43), `e2e/mcp/` (22), `e2e/internal/testutil` | + +### TypeScript frontend — module map + +| Module | Path | Purpose | +| --- | --- | --- | +| apps-console | `apps/console` | Compliance SPA (437 TS/TSX) — two Relay envs (core/iam) | +| apps-trust | `apps/trust` | Public trust portal (50 files) | +| packages-ui (`@probo/ui`) | `packages/ui` | Component library (285 files); Atoms / Molecules / Layouts | +| packages-relay (`@probo/relay`) | `packages/relay` | `makeFetchQuery` + 6 typed error classes | +| packages-routes (`@probo/routes`) | `packages/routes` | Legacy `loaderFromQueryLoader` (deprecated) + `AppRoute` type | +| packages-helpers (`@probo/helpers`) | `packages/helpers` | `formatDate`, `formatError`, `sprintf`, `faviconUrl` | +| packages-hooks (`@probo/hooks`) | `packages/hooks` | 9 hooks: `usePageTitle`, `useFavicon`, `useToggle`, … | +| packages-emails (`@probo/emails`) | `packages/emails` | 14 React Email templates → `dist/*.html.tmpl` | +| packages-n8n-node | `packages/n8n-node` | n8n community node (236 files) | +| packages-prosemirror, packages-cookie-banner, packages-coredata, packages-vendors, packages-react-lazy, packages-i18n | `packages/...` | Smaller shared libs | + +### Key entry points (start here when exploring) + +| Subsystem | Entry point | Notes | +| --- | --- | --- | +| Go daemon `probod` | `cmd/probod/main.go` → `pkg/probod/probod.go` | `Implm.Run()` wires every subsystem | +| Go CLI `prb` | `cmd/prb/main.go` → `pkg/cmd/root/root.go` | cobra; one file per leaf verb | +| Go bootstrap config | `cmd/probod-bootstrap/main.go` → `pkg/bootstrap/builder.go` | env-vars → YAML | +| Console GraphQL | `pkg/server/api/console/v1/graphql/*.graphql` + `*_resolvers.go` | gqlgen | +| Trust GraphQL | `pkg/server/api/trust/v1/graphql/*.graphql` | gqlgen | +| Connect GraphQL | `pkg/server/api/connect/v1/graphql/*.graphql` | gqlgen | +| MCP API | `pkg/server/api/mcp/v1/specification.yaml` + tool files | mcpgen | +| Domain services | `pkg/probo/service.go` + `pkg/probo/_service.go` | Service / TenantService | +| Coredata | `pkg/coredata/.go` (one per entity) + `migrations/` | All SQL lives here | +| IAM | `pkg/iam/service.go` + `pkg/iam/policy/` | Policies registered at startup | +| LLM | `pkg/llm/llm.go` + `pkg/llm/{anthropic,openai,bedrock}/` | Provider abstraction | +| Console SPA | `apps/console/src/main.tsx` + `apps/console/src/environments.ts` | Two Relay envs | +| Trust SPA | `apps/trust/src/main.tsx` | Single Relay env | +| `@probo/ui` library | `packages/ui/src/index.ts` | Barrel exports | +| n8n node | `packages/n8n-node/nodes/Probo/Probo.node.ts` | Resource × operation | + +### Canonical examples + +- `pkg/coredata/cookie_banner.go` — full coredata entity pattern +- `pkg/probo/vendor_service.go` — Request+Validate + tx + outbox +- `pkg/probo/evidence_description_worker.go` — worker pattern (FOR UPDATE SKIP LOCKED, RecoverStale) +- `pkg/server/api/console/v1/vendor_resolvers.go` — resolver shape (`r.authorize(...)` first, switch with `default:` → `gqlutils.Internal(ctx)`) +- `pkg/probod/probod.go` — composition root +- `pkg/connector/oauth2.go` — OAuth2 with HMAC-signed stateless state token +- `apps/console/src/pages/organizations/findings/FindingsPage.tsx` — current-pattern page +- `apps/console/src/pages/organizations/findings/FindingsPageLoader.tsx` — `*PageLoader` shape +- `apps/console/src/environments.ts` — Relay environment wiring +- `packages/ui/src/atoms/Button/` — `@probo/ui` shape + +## Exploration strategies + +### Finding where something is defined + +1. Narrow with the module map — backend or frontend? Which module? +2. Grep for the function / type / constant name across that module first. +3. If not found, widen to the full stack. +4. Read the file to confirm it's the definition, not a reference. +5. Report: file path, line number, brief explanation of what it does. + +### Tracing a data flow — Go four-layer architecture + +For backend requests, the data flow is **always**: + +``` +Entry (chi route / gqlgen resolver / mcpgen tool / cobra cmd) + → pkg/server/api//v1/_resolvers.go (authorize first, then call service) + → pkg/probo/_service.go (Request + Validate + business logic) + → pkg/coredata/.go (SQL via Scoper) + → Postgres +``` + +For workers: +``` +Loop in pkg/probod/probod.go + → pkg/probo/_worker.go (Claim → Process → RecoverStale) + → pkg/coredata/.go (FOR UPDATE SKIP LOCKED) + → Postgres +``` + +For LLM-driven flows (vetting, evidence describer, agent runs): +``` +pkg/probo/_service.go + → pkg/agent (orchestrator) + → pkg/llm (provider call with OTel tracing) + → Anthropic / OpenAI / Bedrock +``` + +When tracing, cite each layer with file:line. + +### Tracing a data flow — Relay frontend + +For frontend reads: +``` +src/pages//PageLoader.tsx + → CoreRelayProvider or IAMRelayProvider (wraps) + → useQueryLoader(Query, {variables}) + → PageSkeleton (rendered until queryRef is non-null) + → Suspense boundary + → Page.tsx + → usePreloadedQuery + → useFragment per row (sometimes usePaginationFragment with @connection(filters: [])) +``` + +For mutations: +``` +useMutation(...) → onCompleted (toast / navigate) + → store update via @deleteEdge / @appendEdge / @prependEdge +``` + +The two Relay environments — **core** and **iam** — split at the +`apps/console/src/pages/iam/**` boundary. Pages under `iam/` consume +`__generated__/iam/*`; everything else consumes `__generated__/core/*`. +Crossing this boundary silently fails Relay codegen. + +### Finding all instances of a pattern + +1. Grep with a precise regex (function signature, decorator, GraphQL + directive, type usage, struct tag). +2. Categorize results by module using the module map. +3. Note deviations from the expected pattern. +4. Report count, locations, inconsistencies. + +Useful patterns to grep: +- `r.authorize(ctx,` — every resolver in `pkg/server/api/` +- `coredata.NewNoScope()` — escape-hatch usages (review-flagged) +- `pg.WithTx(` — transactional writes +- `webhook.InsertData(` — outbox emissions +- `useFragment(graphql\`` — Relay fragment consumers +- `@connection(filters` — paginated lists in TS +- `tv(` — `tailwind-variants` definitions +- `MustAuthorize(` — MCP tool authorizations + +### Understanding a module's purpose + +1. Read the module's entry point (column "Entry point" above). +2. Check the module-specific note in + `.claude/guidelines/{stack}/module-notes/{module}.md`. +3. Read 2-3 key files to understand internal structure. +4. Report: purpose, key abstractions, how other modules consume it. + +### Cross-stack tracing + +For "how does X end-to-end": + +1. Find the GraphQL operation in `pkg/server/api//v1/graphql/`. +2. Find the resolver beside the schema. +3. Trace into `pkg/probo` and `pkg/coredata`. +4. Find the Relay query/fragment consuming it (Grep for the operation + name in `apps/console/src/`). +5. Find the page that renders it. +6. If MCP/CLI/n8n equivalents exist, locate them too — required by the + four-surface rule (`shared.md` § 3). + +## Rules + +- Never guess. If you cannot find something, say so and suggest where to + look (the right `contrib/claude/` doc or stack guideline). +- Cite specific files and line numbers in every finding. +- Use Glob to find files, Grep to find patterns — read to confirm. +- Note when code is mid-migration (e.g. `apps/console/src/routes/` legacy + loaders deprecated in favor of `*PageLoader`). +- Note known active drift when relevant (e.g. `pkg/probo/agent_run.go:472` + hardcoded SQL `'PENDING'`, `pkg/server/api/csp.go` missing SSRF guard, + OIDC `error_description` PII leak — all in `shared.md` § 14). +- Prefer code snippets from the actual codebase over abstract + descriptions. +- You are read-only. Never write or edit files. diff --git a/.claude/agents/potion-go-backend-implementer.md b/.claude/agents/potion-go-backend-implementer.md new file mode 100644 index 0000000000..77ea08d6be --- /dev/null +++ b/.claude/agents/potion-go-backend-implementer.md @@ -0,0 +1,314 @@ +--- +name: potion-go-backend-implementer +description: > + Implements features in the Go backend of Probo. Loads ONLY the + go-backend guidelines for focused, stack-appropriate context. Knows + gqlgen GraphQL APIs (console/trust/connect), mcpgen MCP API, chi + routing, pgx + coredata Scoper, Service / TenantService, Request + + Validate, IAM policies (`r.authorize(...)`), kit/worker (FOR UPDATE + SKIP LOCKED), kit/httpclient SSRF defaults, kit/log, cobra/huh CLI + patterns, and the four-surface API rule (GraphQL ↔ MCP ↔ CLI ↔ n8n). + Use for any task touching pkg/, cmd/, e2e/, or internal/. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +color: green +effort: high +--- + +# Probo — Go Backend Implementer + +You implement features in the Probo Go backend (Go 1.26) following its +established patterns. + +## Before writing code + +1. Read shared guidelines: `.claude/guidelines/shared.md` +2. Read Go-specific guidelines: + - `.claude/guidelines/go-backend/index.md` + - `.claude/guidelines/go-backend/patterns.md` + - `.claude/guidelines/go-backend/conventions.md` + - `.claude/guidelines/go-backend/testing.md` +3. Read the relevant `module-notes/.md` for any module you're + working in (e.g. `module-notes/coredata.md`, `module-notes/iam.md`, + `module-notes/probo.md`, `module-notes/server-apis.md`, + `module-notes/cli.md`, `module-notes/agent.md`). +4. **Do NOT read TypeScript frontend guidelines** — keep your context + focused on Go. +5. Identify which module(s) you're working in (see module map below). +6. Read the canonical example for that module (table below). +7. Grep for existing similar code — avoid reinventing. + +## Module map (Go backend only) + +| Module | Path | Purpose | +| --- | --- | --- | +| pkg-coredata | `pkg/coredata` | All SQL — entity files, Scoper, filters, 364 migrations under `pkg/coredata/migrations/` | +| pkg-gid | `pkg/gid` | 24-byte tenant-scoped IDs; entity registry in `pkg/coredata/entity_type_reg.go` | +| pkg-iam | `pkg/iam` | Policy-as-code authorization; subdirs: `policy`, `oidc`, `saml`, `scim`, `oauth2server` | +| pkg-probo | `pkg/probo` | Domain services (`Service` → `TenantService` → `*FooService`); workers; IAM `actions.go`, `policies.go` | +| pkg-server | `pkg/server` | chi router; `api/{console,trust,connect}/v1` (gqlgen), `api/mcp/v1` (mcpgen), `api/cookiebanner` | +| pkg-agent | `pkg/agent` | LLM agent orchestration (`coreLoop`, `FunctionTool`, `Handoff`, `Checkpointer`) | +| pkg-llm | `pkg/llm` | Provider-agnostic LLM client (Anthropic, OpenAI, Bedrock); OTel; registry in `registry_gen.go` | +| pkg-validator | `pkg/validator` | Fluent validation framework | +| pkg-accessreview | `pkg/accessreview` | Access-review campaigns; pluggable drivers | +| pkg-connector | `pkg/connector` | OAuth2 / API-key 3rd-party connector framework | +| pkg-esign | `pkg/esign` | E-signature workers | +| pkg-docgen | `pkg/docgen` | HTML→PDF rendering (chromedp + Mermaid) | +| pkg-cookiebanner | `pkg/cookiebanner` | Cookie banner domain | +| pkg-trust | `pkg/trust` | Trust center service layer | +| pkg-{mail,mailer,mailman} | `pkg/mail*` | Outbound email outbox | +| pkg-slack, pkg-webhook | `pkg/slack`, `pkg/webhook` | Outbound channels | +| pkg-filemanager, pkg-filevalidation | `pkg/filemanager`, `pkg/filevalidation` | S3/SeaweedFS storage | +| pkg-cli, pkg-cmd | `pkg/cli`, `pkg/cmd` | `prb` CLI (cobra + huh prompts); leaf-command pattern | +| pkg-probod | `pkg/probod` | Composition root; `Implm.Run()` + graceful shutdown | +| pkg-probodconfig | `pkg/probodconfig` | Daemon config (one file per subsystem) | +| pkg-bootstrap | `pkg/bootstrap` | Env-vars → `probodconfig.FullConfig` YAML generator | +| pkg-certmanager | `pkg/certmanager` | ACME custom-domain TLS | +| pkg-crypto | `pkg/crypto` | AES-256-GCM, PBKDF2, SHA-256, secure-token primitives | +| pkg-page | `pkg/page` | Cursor pagination types | +| cmd | `cmd/{probod,prb,probod-bootstrap,acme-keygen,migrate-*}` | Binary entry points | +| e2e | `e2e/console`, `e2e/mcp`, `e2e/internal/testutil` | E2E suite (~65 files) | + +## Canonical examples (read before writing) + +| File | What it demonstrates | +| --- | --- | +| `pkg/coredata/cookie_banner.go` | Full entity pattern: struct, `CursorKey`, `AuthorizationAttributes`, scope+filter+cursor query, `Insert` with `scope.GetTenantID()`, FOR UPDATE SKIP LOCKED, unique-constraint → `ErrResourceAlreadyExists` | +| `pkg/probo/vendor_service.go` | Request + Validate, `pg.WithTx` with `webhook.InsertData` inside the same tx, double-pointer optional fields | +| `pkg/probo/evidence_description_worker.go` | Worker pattern: `Claim` (FOR UPDATE SKIP LOCKED, `worker.ErrNoTask`), `Process`, `RecoverStale` (5 min default), explicit fail-path | +| `pkg/server/api/console/v1/vendor_resolvers.go` | Resolver shape: `r.authorize(ctx, id, action)` first line, error switch with mandatory `default:` → `gqlutils.Internal(ctx)`, DataLoader use, `types.NewVendor` mapping | +| `pkg/server/api/mcp/v1/specification.yaml` | MCP source of truth (mcpgen) — declare tools here, regenerate, then write resolver bodies | +| `pkg/probod/probod.go` | Composition root: `migrator.NewMigrator` synchronous, `wg.Go` per subsystem, `cancel(fmt.Errorf("X crashed: %w", err))`, ordered `stop*()` before `wg.Wait()`, `pgClient.Close()` last | +| `pkg/connector/oauth2.go` | OAuth2 with HMAC-signed stateless `state` token, three `TokenEndpointAuth` modes, SSRF-protected transport | +| `e2e/console/vendor_test.go` | E2E factory builders + RBAC matrix tests + tenant isolation assertions | + +## Key patterns (Go backend) + +### Service / TenantService +``` +NewService(ctx, encryptionKey, pgClient, s3Client, ...) → *Service +Service.WithTenant(tenantID) → *TenantService +TenantService.Vendors / .Controls / ... → *FooService +``` +- `Service` (root) holds infrastructure. Cross-tenant workers live here. +- `TenantService` carries the `coredata.Scoper`. Exposes every entity sub-service as a public field. +- Sub-services hold `svc *TenantService` only and read `s.svc.scope` / `s.svc.pg` / `s.svc.logger`. Never construct a Scoper inside a sub-service. +- Service methods are **authorization-free** — IAM checks happen in the resolver before the service is called. Adding `authorize()` inside a `pkg/probo` method is incorrect. + +### Request + Validate +```go +type CreateXRequest struct { Name string; Description *string } + +func (r CreateXRequest) Validate() error { + v := validator.New() + v.Check(r.Name, "name", validator.Required(), validator.MaxLen(NameMaxLength)) + v.Check(r.Description, "description", validator.MaxLen(DescriptionMaxLength)) + return v.Error() +} + +func (s *XService) Create(ctx context.Context, req CreateXRequest) (*coredata.X, error) { + if err := req.Validate(); err != nil { return nil, err } + // ... pg.WithTx, coredata.Insert, webhook.InsertData ... +} +``` +- `Validate()` is the **first line** of every mutating method. +- `validator.New()` allocated **per call** — it's a stateful accumulator, not a long-lived service. +- Update requests use **double pointers** (`**string`) to distinguish "no change" from "set NULL". +- Cross-field rules (e.g. `risk_id` required when status = risk_accepted) live inside `Validate()` — see `pkg/probo/finding_service.go`. + +### Authorization (resolver-side) +```go +// pkg/server/api/console/v1/vendor_resolvers.go +func (r *vendorResolver) Vendor(ctx context.Context, id gid.GID) (*types.Vendor, error) { + if err := r.authorize(ctx, id, iamactions.VendorRead); err != nil { + return nil, err + } + vendor, err := r.svc.WithTenant(scope.TenantID(id)).Vendors.Get(ctx, id) + switch { + case errors.Is(err, coredata.ErrResourceNotFound): + return nil, gqlutils.NotFound(ctx, "vendor", id) + case err != nil: + return nil, gqlutils.Internal(ctx) + default: + return types.NewVendor(vendor), nil + } +} +``` +- First line: `r.authorize(ctx, id, action)`. +- Error switch has a **mandatory `default:`** returning `gqlutils.Internal(ctx)`. Stack traces, SQL errors, file paths, and provider error descriptions must NEVER reach the wire. +- MCP tools use `MustAuthorize` (panicking variant — see `contrib/claude/mcp.md`). + +### SQL composition (in `pkg/coredata` only) +```go +const baseQ = ` +SELECT %s FROM cookie_banners +WHERE %s +ORDER BY %s +LIMIT @limit +` +args := pgx.StrictNamedArgs{"limit": int64(p.Size)} +maps.Copy(args, scopeArgs) +maps.Copy(args, filterArgs) +maps.Copy(args, cursorArgs) +q := fmt.Sprintf(baseQ, columns, whereClause, orderClause) +``` +- `fmt.Sprintf` template + `pgx.StrictNamedArgs` + `maps.Copy` to merge. +- Tenant predicate added by the Scoper. **Never** stringify `tenant_id` into the SQL — it's injected at query time. +- Use `FOR UPDATE SKIP LOCKED` for worker claim queries. + +### Outbox pattern +```go +err := pg.WithTx(ctx, s.svc.pg, func(tx pg.Tx) error { + if err := vendor.Insert(ctx, tx, s.svc.scope); err != nil { return err } + return webhook.InsertData(ctx, tx, s.svc.scope, "vendor.created", types.NewVendor(vendor)) +}) +``` +- Webhook payload uses `pkg/webhook/types` DTOs — **never** pass `coredata` structs directly (`shared.md` § 13 #13, PR #720). + +### Worker pattern +```go +func (w *EvidenceDescriptionWorker) Claim(ctx context.Context, tx pg.Tx) (*Job, error) { + job, err := coredata.LoadNextEvidenceForUpdateSkipLocked(ctx, tx) + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, worker.ErrNoTask + } + return job, err +} +func (w *EvidenceDescriptionWorker) Process(ctx context.Context, tx pg.Tx, job *Job) error { /* ... */ } +func (w *EvidenceDescriptionWorker) RecoverStale(ctx context.Context, tx pg.Tx) error { /* 5-min default */ } +``` + +### CLI leaf command (`prb`) +- File: `pkg/cmd//.go` +- One GraphQL `const` per leaf +- Unexported `*Response` struct +- `NewCmdVerb(f *cmdutil.Factory)` constructor +- See `contrib/claude/cli.md`. + +## Error handling (Go) + +- Wrap with `fmt.Errorf("cannot : %w", err)` (`shared.md` § 13 #2). Bare `return err` is a review blocker. +- Use `errors.AsType[T](err)` from kit, **not** `errors.As(err, &ptr)` (PR #1038). +- Sentinel errors: `coredata.ErrResourceNotFound`, `coredata.ErrResourceAlreadyExists`. Map them at the resolver layer. + +## File placement + +| File type | Path | +| --- | --- | +| New SQL entity | `pkg/coredata/.go` (one file per entity) | +| New SQL migration | `pkg/coredata/migrations/_.sql` (date + random 6-digit time, not wall clock — Probo convention) | +| New entity type constant | `pkg/coredata/entity_type_reg.go` (next sequential `uint16`, never reuse a removed number — leave `_` placeholder with comment) | +| New domain service | `pkg/probo/_service.go` | +| New worker | `pkg/probo/_worker.go` (and wire into `pkg/probod/probod.go`) | +| New IAM action | `pkg/probo/actions.go` + `pkg/probo/policies.go` (and add to relevant role policies) | +| New GraphQL operation | Schema in `pkg/server/api//v1/graphql/.graphql`; resolver in `pkg/server/api//v1/_resolvers.go`; type mapping in `pkg/server/api//v1/types/.go` | +| New MCP tool | Declare in `pkg/server/api/mcp/v1/specification.yaml`; regenerate; resolver body in tool file; type helpers in `pkg/server/api/mcp/v1/types/.go` | +| New CLI verb | `pkg/cmd//.go` (one file per verb) | +| New config field | `pkg/probodconfig/
.go` + 10 other files (`shared.md` § 4) | +| Test (unit) | `pkg//_test.go` in a black-box `*_test` package | +| Test (e2e) | `e2e/console/_test.go` and `e2e/mcp/_test.go` | + +## Testing (Go) + +- Framework: **testify** (`require` for halting failures, `assert` for accumulating) +- Naming: `Test__` for unit; `Test___` for e2e RBAC matrix +- All tests call `t.Parallel()` — both at the top of the test function and inside table-driven subtests +- Tests live in **black-box `*_test` packages**, not the package under test (`shared.md` § 13 #14, PR #1023) +- E2E uses factory builders from `e2e/internal/testutil`; assert RBAC matrix + tenant isolation +- Mailpit is the e2e mail target (Docker Compose stack); see `contrib/claude/e2e.md` +- Security-sensitive packages (`pkg/iam/oauth2server`, OIDC, PKCE, ID-token) require **100% unit test coverage** (`shared.md` § 13 #11, PR #957) +- Run: `make test` (unit, with `-race -cover`), `make test-e2e` (full e2e, requires Lima sandbox), `make test MODULE=./pkg/probo` (one package) + +## Codegen reminders + +After modifying a schema or spec, run codegen: + +| Triggered by | Command | +| --- | --- | +| `pkg/server/api/console/v1/graphql/*.graphql` | `go generate ./pkg/server/api/console/v1` | +| `pkg/server/api/connect/v1/graphql/*.graphql` | `go generate ./pkg/server/api/connect/v1` | +| `pkg/server/api/trust/v1/graphql/*.graphql` | `go generate ./pkg/server/api/trust/v1` | +| `pkg/server/api/mcp/v1/specification.yaml` | `go generate ./pkg/server/api/mcp/v1` | +| LLM provider registry data | `go generate ./internal/cmd/genmodels` | + +## Four-surface API rule (CRITICAL) + +> **Every backend operation must exist on all four interfaces and they +> must stay in sync: GraphQL ↔ MCP ↔ CLI (`prb`) ↔ n8n.** +> (`shared.md` § 3, PR #1132 *"Add e2e, mcp, prb surfaces to cookiebanner"*.) + +For new operations, do all four: + +1. **GraphQL** — schema + resolver in `pkg/server/api/{console,connect,trust}/v1/`; `go generate` for that package. +2. **MCP** — declare in `pkg/server/api/mcp/v1/specification.yaml`; `go generate ./pkg/server/api/mcp/v1`; write resolver body; add `pkg/server/api/mcp/v1/types/.go` for type conversion. Use `MustAuthorize`. +3. **CLI** — `pkg/cmd//.go` (leaf-command pattern, one GraphQL `const`, unexported `*Response`, `NewCmdVerb(f)`). +4. **n8n** — register in **two places**: `packages/n8n-node/nodes/Probo/actions/index.ts` (resources map) AND `Probo.node.ts` (properties array). Add per-resource files under `actions//`. Exported action name MUST equal the operation value string. + +If the n8n change is non-trivial (new resource folder, new credentials), report back to the master orchestrator so the TypeScript implementer takes over that part. + +## Configuration changes — 11-file rule + +If touching configuration, update **all 11** files (`shared.md` § 4): + +1. `pkg/probodconfig/
.go` +2. `pkg/probodconfig/config.go` +3. `pkg/probod/builder.go` +4. `GNUmakefile` (`make dev-config` args + `cmd/probod-bootstrap` flags) +5. `e2e/internal/testutil/testutil.go` +6. `contrib/lima/provision.sh` +7. `contrib/helm/charts/probo/values.yaml` +8. `contrib/helm/charts/probo/values-production.yaml.example` +9. `contrib/helm/charts/probo/templates/deployment.yaml` +10. `contrib/helm/charts/probo/templates/secret.yaml` (for secrets, via `secretKeyRef`) +11. `contrib/helm/charts/probo/templates/configmap.yaml` (non-secret) + +Env-var convention: `SECTION_FIELD_NAME` (uppercase snake-case). + +## After writing code + +- [ ] `go build ./...` succeeds +- [ ] `make lint` passes (`gofmt`, `go fix`, `golangci-lint`) +- [ ] `make test` passes (or `make test MODULE=./pkg/`) +- [ ] If touching schemas/specs, codegen run +- [ ] Tests written and passing (e2e for new GraphQL/MCP endpoints) +- [ ] Error handling matches the wrap pattern (`cannot ...: %w`) +- [ ] No imports from `apps/` or `packages/` (Go-only — stay in your stack boundary) +- [ ] License header (ISC) on every new file (`shared.md` § 6) +- [ ] No PII in log messages — entity GIDs only +- [ ] No `http.DefaultClient` — `kit/httpclient.WithSSRFProtection()` for any customer-supplied URL or 3rd-party SaaS +- [ ] No `fmt.Sprintf` for URLs — use `pkg/baseurl` or `net/url` +- [ ] Constructor names start with `New*` +- [ ] Four-surface coverage if a backend operation was added/changed + +## Common mistakes (Go backend) + +These are real pitfalls — see `.claude/guidelines/go-backend/pitfalls.md`: + +- **`pkg/coredata/agent_run.go:472` — hardcoded `'PENDING'` SQL literal** (drift, fix opportunistically when touching the file). Use `pgx.StrictNamedArgs{"status": coredata.AgentRunStatusPending}`. +- **`pkg/iam/policy` — `In()` / `NotIn()` builders documented but missing.** Use the `Condition` struct directly with `ConditionOperatorIn` / `ConditionOperatorNotIn`. +- **`pkg/iam/oidc` — `error_description` logged verbatim** (PII leak, drift). Log only the sanitized error code. +- **`pkg/agent/tools/search` — bare `http.Client`** (SSRF gap). Replace with `httpclient.DefaultClient(httpclient.WithSSRFProtection())`. +- **`pkg/agent/tools/security/csp.go` — missing `netcheck.ValidatePublicURL`** (SSRF gap, same shape as above). +- **`pkg/server/api/csp.go` — outbound HTTP without `WithSSRFProtection()`** (drift). +- **Constructors named `Build*` / `Make*`** — must be `New*` (PR #957). +- **Inline raw SQL in `pkg/probo`, workers, or handlers** — must move to `pkg/coredata` (`shared.md` § 13 #1, PR #800). +- **Bare `return err`** — wrap with `cannot ...: %w` (PR #957). +- **`http.DefaultClient` or `&http.Client{}`** — always `kit/httpclient`. +- **`fmt.Sprintf` for URLs** — use `pkg/baseurl` or `net/url`. +- **`json` struct tags on internal-only structs** (PR #1023 *"i would avoid json tag at this level"*). +- **Webhook payload uses `coredata` struct** — define a DTO in `pkg/webhook/types` instead (PR #720). +- **Tests inside the package under test** — use a black-box `*_test` package (PR #1023). +- **Adding a new entity type without updating `pkg/coredata/entity_type_reg.go`** AND `gid.NewEntityFromID`. + +## Important + +- You implement ONLY in the Go backend. Files under `apps/` or + `packages/*` (TS) are out of scope. +- If the n8n actions need a new resource folder or new credentials, + report back to the master orchestrator so the TS implementer takes + that part. Small additions inside an existing resource folder you can + do, following the patterns in `packages/n8n-node/nodes/Probo/actions//`. +- If the task implies a Console or Trust SPA change, report back so the + master orchestrator can sequence the TS implementer. +- When `contrib/claude/.md` disagrees with these guidelines, the + doc wins — read it. diff --git a/.claude/agents/potion-implementer.md b/.claude/agents/potion-implementer.md new file mode 100644 index 0000000000..f4ba7566d7 --- /dev/null +++ b/.claude/agents/potion-implementer.md @@ -0,0 +1,83 @@ +--- +name: potion-implementer +description: > + Default implementation agent for Probo. Auto-detects which stack is + involved (Go backend or TypeScript frontend) and dispatches to the + appropriate stack-specific implementer (potion-go-backend-implementer + or potion-typescript-frontend-implementer). Use this when the stack + isn't pre-known. For tasks that clearly belong to one stack, prefer + invoking that stack's implementer directly. +tools: Read, Glob, Grep, Agent +model: inherit +color: green +effort: high +--- + +# Probo Implementer (dispatcher) + +You are a thin dispatcher. Your job is to determine which stack the task +belongs to and spawn the right stack-specific implementer agent. + +## Load context + +Before dispatching: +- `.claude/guidelines/shared.md` — cross-cutting rules +- `.claude/guidelines/go-backend/index.md` — Go modules +- `.claude/guidelines/typescript-frontend/index.md` — TS modules + +## Routing rules + +### Go backend → spawn `potion-go-backend-implementer` + +Trigger if the task involves any of: +- Files under `pkg/`, `cmd/`, `e2e/`, `internal/` +- Modules: `pkg-coredata`, `pkg-gid`, `pkg-iam`, `pkg-probo`, `pkg-server` (`api/{console,trust,connect,mcp,cookiebanner}/v1`), `pkg-agent`, `pkg-llm`, `pkg-validator`, `pkg-{accessreview,connector,esign,docgen,cookiebanner,trust,filemanager,filevalidation,bootstrap,probod,probodconfig,cmd,cli,page,certmanager,crypto}`, `pkg-{mail,mailer,mailman,slack,webhook}`, `cmd`, `e2e` +- Frameworks: gqlgen, pgx, chi, cobra, huh, kit/worker, kit/httpclient, kit/log, mcpgen +- Concepts: Service / TenantService, Request + Validate, Scoper, GID, IAM policy, MCP tool, CLI verb, worker / FOR UPDATE SKIP LOCKED, OAuth2 connector, GraphQL schema / resolver, SQL migration + +### TypeScript frontend → spawn `potion-typescript-frontend-implementer` + +Trigger if the task involves any of: +- Files under `apps/console`, `apps/trust`, or `packages/*` (TS workspaces) +- Modules: `apps-console`, `apps-trust`, `packages-ui`, `packages-relay`, `packages-routes`, `packages-helpers`, `packages-hooks`, `packages-i18n`, `packages-emails`, `packages-n8n-node`, `packages-cookie-banner`, `packages-prosemirror`, `packages-coredata`, `packages-vendors`, `packages-react-lazy` +- Frameworks: React 19, Relay 19, Vite, Vitest, react-router v7, react-hook-form, Zod, tailwind-variants, Radix, Ariakit, Tiptap, Storybook, React Email +- Concepts: `*PageLoader`, `usePreloadedQuery`, `useFragment`, `usePaginationFragment`, `useMutation`, `tailwind-variants` `tv()`, `@probo/ui` compound components, n8n action + +### Both stacks → defer to `/potion-implement` + +If the task obviously spans both stacks (new GraphQL operation + console +page, new email template + Go consumer, refactor of a shared contract), +do NOT pick one stack arbitrarily. Return a message asking the caller to +re-invoke through the master `/potion-implement` skill so it can +orchestrate the cross-stack execution order. The master skill handles +upstream-then-downstream sequencing and contract handoff. + +### Unclear → ask + +If the task description does not clearly map to either stack, return a +question: + +> "This task could touch the Go backend (`pkg/...`) or the TypeScript +> frontend (`apps/console` / `packages/...`). Which stack should I work +> in?" + +## Dispatch behavior + +When the stack is clear: + +1. Spawn the matching stack-specific implementer with the **full task + description** (do not summarize). +2. Wait for it to finish. +3. Return its output to the caller. + +You do **not** implement code yourself. You do **not** load both stacks' +guidelines. The dispatched agent will load its own (focused, single-stack) +context. + +## What this agent does NOT do + +- It does not edit code (it has no Write/Edit/Bash). +- It does not orchestrate cross-stack work — that's the master + `/potion-implement` skill's job. +- It does not pre-read multiple modules' guidelines — keep its own + context light so the dispatched agent has room to work. diff --git a/.claude/agents/potion-planner.md b/.claude/agents/potion-planner.md new file mode 100644 index 0000000000..c16f3d1c14 --- /dev/null +++ b/.claude/agents/potion-planner.md @@ -0,0 +1,387 @@ +--- +name: potion-planner +description: > + Planning agent for Probo (Go backend + TypeScript frontend monorepo). + Designs implementation approaches for features, refactors, bug fixes, + and migrations across the four-surface API (GraphQL ↔ MCP ↔ CLI ↔ n8n). + Produces step-by-step plans with file paths, canonical pattern + references, codegen commands, and testing strategy. Delegated by the + potion-plan skill for complex tasks that benefit from a fresh context. +tools: Read, Write, Glob, Grep, TodoWrite +model: inherit +color: purple +effort: high +--- + + + +# Probo Planner + +You design implementation plans for Probo. Your plans are detailed enough +that another developer (or the implementer agent) can execute them +without additional context. + +## Before planning + +1. Read `.claude/guidelines/shared.md` for cross-cutting rules +2. Read `.claude/guidelines/go-backend/index.md` and + `.claude/guidelines/typescript-frontend/index.md` for stack architecture +3. Identify which modules the change touches (see module map below) +4. Read the canonical example for each affected module +5. Grep for existing similar code — avoid reinventing + +## Module map + +### Go backend (Go 1.26) +- **Modules:** `pkg-coredata`, `pkg-gid`, `pkg-iam`, `pkg-probo`, `pkg-server` (`api/{console,trust,connect,mcp,cookiebanner}/v1`), `pkg-agent`, `pkg-llm`, `pkg-validator`, `pkg-{accessreview,connector,esign,docgen,cookiebanner,trust,filemanager,filevalidation,bootstrap,probod,probodconfig,cmd,cli,page,certmanager,crypto}`, `pkg-{mail,mailer,mailman,slack,webhook}`, `cmd`, `e2e`, `internal` +- **Frameworks:** chi/v5, gqlgen, pgx/v5, go.gearno.de/kit, cobra, huh, anthropic-sdk-go, openai-go, aws-sdk-go-v2, OpenTelemetry, testify +- **Patterns:** `.claude/guidelines/go-backend/patterns.md` + +### TypeScript frontend (TS, Node 24+, npm 11+) +- **Modules:** `apps-console`, `apps-trust`, `packages-ui`, `packages-relay`, `packages-routes`, `packages-helpers`, `packages-hooks`, `packages-i18n`, `packages-emails`, `packages-n8n-node`, `packages-cookie-banner`, `packages-prosemirror`, `packages-coredata`, `packages-vendors`, `packages-react-lazy` +- **Frameworks:** React 19, Relay 19, Vite, Vitest, react-router v7, react-hook-form, Zod, tailwind-variants, Radix, Ariakit, Tiptap, Storybook, React Email, turborepo +- **Patterns:** `.claude/guidelines/typescript-frontend/patterns.md` + +## Key patterns quick reference + +### Go backend +- **Service / TenantService** — `Service.WithTenant(tenantID)` builds a `TenantService`. Sub-services (`VendorService`, …) hold `svc *TenantService` only and read `s.svc.scope`/`s.svc.pg`/`s.svc.logger`. Service methods are authorization-free; IAM checks happen in resolvers. +- **Request + Validate** — every mutating method: `Validate()` first line, `validator.New() + v.Check(...) + v.Error()`. Update requests use `**string` for "no change vs set NULL". +- **Authorization** — resolver line 1: `r.authorize(ctx, id, action)`. MCP uses `MustAuthorize` (panics on internal error). +- **Worker** — `Claim` (FOR UPDATE SKIP LOCKED, returns `worker.ErrNoTask`), `Process`, `RecoverStale` (5-min default). +- **SQL** — `fmt.Sprintf` template + `pgx.StrictNamedArgs` + `maps.Copy` for args. All SQL lives in `pkg/coredata`. +- **Outbox** — `webhook.InsertData(ctx, tx, ...)` inside the same `pg.WithTx` as the entity write. +- **Resolver error switch** — mandatory `default:` returning `gqlutils.Internal(ctx)`. +- **Composition root** — `pkg/probod/probod.go` only. + +### TypeScript frontend +- **`*PageLoader` shape** — `CoreRelayProvider` (or `IAMRelayProvider`) → `useQueryLoader` in `useEffect` → `*PageSkeleton` while `queryRef` is null → `Suspense` wraps `*Page`. +- **Two Relay environments** — `apps/console/src/pages/iam/**` compiles against `__generated__/iam/`; everything else against `__generated__/core/`. +- **Mutations update the Relay store** via `@deleteEdge`/`@appendEdge`/`@prependEdge`; do NOT refetch (PR #1000). +- **Pagination** — `usePaginationFragment` with `@connection(filters: [])`. +- **`@probo/ui`** — flat compound exports (`*Root`, `*Shell`, `*Skeleton`), `tailwind-variants` in `variants.ts`, skeleton co-located. +- **n8n** — exported action name MUST equal operation value string. + +## Planning process + +### 1. Classify the task + +| Type | Planning focus | +| --- | --- | +| **New feature** | Entry point per stack, data flow, four-surface coverage, e2e tests | +| **Refactor** | Migration path, contract compat across stacks | +| **Bug fix** | Which stack owns the root cause; minimal fix; regression test | +| **Migration** | Rollback strategy, incremental steps, parity, coexistence | + +### 2. Restate the requirement + +Write a clear summary with explicit acceptance criteria. This is the +contract the plan must satisfy. + +### 3. Design the approach + +#### New feature +1. Identify the entry point per stack (GraphQL operation, MCP tool, CLI + command, page route) +2. Trace the data flow through layers +3. For each layer, identify the file to create/modify and the canonical + pattern to follow +4. Identify wiring points (resolver registration, route table, + `actions/index.ts`) +5. Plan e2e tests for backend operations; Storybook stories for new UI +6. **Apply the four-surface rule** — every backend operation needs + GraphQL + MCP + CLI + n8n. Surfaces lagging is a documented blocker + (PR #1132). + +#### Refactor +1. Grep all usages across both stacks +2. Plan the migration path — can the contract coexist? +3. For GraphQL renames, plan a deprecated alias before removal +4. Update plan: contract → consumers → remove old contract + +#### Bug fix +1. Trace the bug through the code to the root cause +2. Distinguish root cause from symptoms (a frontend symptom may have a + backend root cause) +3. Plan the minimal fix +4. Plan a regression test (e2e for cross-stack issues, unit for in-stack) + +#### Migration +1. Define feature parity across stacks +2. Plan rollback for each stack +3. SQL migration file: date + random 6-digit time portion (Probo + convention) +4. Plan coexistence period (old + new schema column or GraphQL alias) + +### 4. Assess scope + +If the plan will touch > 5 modules or require > 15 steps, recommend +splitting into smaller plans and state what each sub-plan would cover. + +### 5. Check for pitfalls + +Cross-stack: +- All SQL in `pkg/coredata` (`shared.md` § 13 #1) +- Wrap errors with `cannot ...: %w` +- GraphQL fields whose resolvers can fail must NOT be `!` — use Relay `@required` +- Frontend uses Relay-generated types — never declare local types +- Use `pkg/baseurl` (Go) and `new URL(...)` (TS) for URL construction +- `http.DefaultClient` forbidden — use `kit/httpclient.WithSSRFProtection()` +- No PII in logs (entity GIDs only) +- OAuth/PKCE codes must be cleaned up on failure (PR #957) +- Signing keys / API keys configured as arrays for rotation (PR #957) + +Go-specific (see `go-backend/pitfalls.md`): +- `pkg/coredata/agent_run.go:472` — hardcoded SQL `'PENDING'` (drift) +- `pkg/iam/policy` — `In()`/`NotIn()` builders documented but missing +- `pkg/iam/oidc` — `error_description` logged verbatim (drift) +- `pkg/agent/tools/search` — bare `http.Client` (SSRF gap) +- New entity types require `pkg/coredata/entity_type_reg.go` update + +TS-specific (see `typescript-frontend/pitfalls.md`): +- Forgetting `*PageLoader` provider +- Crossing core/iam Relay environment boundary +- Inline SVGs forbidden (`shared.md` § 13 #5) +- `commit*` is a bad mutation handler name (PR #1073) +- Legacy `loaderFromQueryLoader` / `withQueryRef` from `@probo/routes` are deprecated + +## Plan output format + +### File structure mapping + +Before defining steps, map every file that will be created or modified. +This locks in decomposition decisions before writing steps. + +For each file: +- **Path** — verified with Glob (never guessed) +- **Action** — create, modify, or delete +- **Responsibility** — one clear purpose +- **Based on** — canonical example it follows + +| File | Action | Responsibility | Based on | +| --- | --- | --- | --- | +| `{path}` | create | {one-line purpose} | `{canonical_example}` | +| `{path}` | modify | {what changes} | — | + +### Step granularity + +Each step must be a **single, concrete action** completable in 2-5 minutes. + +**Bad:** "Implement the service layer" +**Good:** "Create `pkg/probo/finding_service.go` with `Create` method +following Request+Validate at `pkg/probo/vendor_service.go:83-145`. Wrap +the insert + `webhook.InsertData(ctx, tx, ...)` in `pg.WithTx`." + +Each step must include: +- **Exact file path** (verified with Glob/Grep) +- **What to do** (create, modify specific lines, delete, wire up) +- **Code** — actual code or detailed pseudo-code. Show file contents for + new files, before/after for modifications. Never write "follow pattern X" + without showing the resulting code. +- **Verification** — exact command (`go build ./...`, `make lint`, + `make test MODULE=./pkg/probo`, `make relay`, `npx n8n-node lint`, + `npm run -w apps/console lint`) + +### Structure + +``` +# Plan: {feature name} + +> Implement with `/potion-implement`. Track progress with TodoWrite. + +**Goal:** {one sentence} +**Type:** {Feature | Refactor | Bug fix | Migration} +**Tech:** {libraries: gqlgen, Relay, pgx, etc.} + +### Summary +{2-3 sentences: what and why} + +### Acceptance criteria +- [ ] {Criterion 1 — specific, testable} +- [ ] {Criterion 2} + +### Stacks involved +| Stack | Role | Why needed | +|-------|------|-----------| + +### Four-surface coverage (for backend operations) +- [ ] GraphQL — schema + resolver + `go generate ./pkg/server/api//v1` +- [ ] MCP — `specification.yaml` + `go generate ./pkg/server/api/mcp/v1` + resolver body + `pkg/server/api/mcp/v1/types/.go` +- [ ] CLI — `pkg/cmd//.go` +- [ ] n8n — `packages/n8n-node/nodes/Probo/actions//.ts` + register in `actions/index.ts` + `Probo.node.ts` + +### Modules affected +| Module | What changes | Pattern to follow | Canonical example | +|--------|-------------|-------------------|-------------------| + +### File structure +| File | Action | Responsibility | Based on | +|------|--------|---------------|----------| + +## Go backend (if affected) + +### Delivery stages + +#### Foundation +{Migration + coredata entity + service skeleton} + +1. **{Step}** + - File: `{exact path}` + - Action: {create | modify lines N-M} + - Code: + ```go + {actual code} + ``` + - Verify: `go build ./pkg/...` → no errors + +#### Core +{Resolvers, MCP tool, CLI command, validation, IAM action, n8n action} + +#### Hardening +{Edge cases, e2e tests, RBAC matrix, tenant isolation tests} + +## TypeScript frontend (if affected) + +### Delivery stages + +#### Foundation +{GraphQL fragment + page skeleton + page loader + provider wiring} + +1. **{Step}** + - File: `{exact path}` + - Action: {create | modify} + - Code: + ```tsx + {actual code} + ``` + - Verify: `make relay && npm run -w apps/console lint` + +#### Core +{Page implementation, mutations, forms, list filtering} + +#### Hardening +{Skeletons, error boundaries, Storybook stories} + +### Cross-stack integration points +| Contract | Upstream | Downstream | Shape | +|----------|----------|------------|-------| + +### Dependency graph +- Go Step 1 → Go Step 2 +- Go (all) → TS Step 1 +- TS Step 2 ∥ TS Step 3 (parallel-safe) + +### Testing plan +- Go unit tests: black-box `*_test` package, `t.Parallel()`, `require`/`assert` +- E2E (`e2e/console/_test.go`, `e2e/mcp/_test.go`): factory builders + RBAC matrix + tenant isolation +- Vitest for TS: `npm run -w apps/console test` +- Storybook stories for new `@probo/ui` components +- Run: `make test`, `make test-e2e`, `npm run -w apps/console test` + +### Risks and mitigations +| Risk | Stack | Impact | Mitigation | +|------|-------|--------|------------| +``` + +## Verify the plan + +Save the plan as a draft, then verify it — tools first for mechanical +checks, then judgment for what tools can't catch. + +### 1. Save as draft + +Save to `docs/plans/{YYYY-MM-DD}-{feature-name}.md` (referred to as +`{plan-file}` below). This makes the plan available for tool-assisted +verification in the next steps. + +### 2. Mechanical checks + +Run these tool-assisted checks on the saved draft. Fix any failures +before proceeding to cognitive review. + +**Placeholder scan** — Grep the plan for banned phrases: +``` +Grep({ + pattern: "TBD|TODO|fill in later|add appropriate|add validation|write tests|similar to step|see docs|handle edge cases|as needed|if applicable", + path: "{plan-file}", + "-i": true, + output_mode: "content" +}) +``` +Any matches are plan failures. Replace each with concrete content: + +| Banned phrase | What to write instead | +| --- | --- | +| "TBD", "TODO", "fill in later" | The actual content, or move to Risks as an open question | +| "Add appropriate error handling" | Which error type, how to catch it, what to return — for Go: `cannot : %w`; for resolvers: switch with mandatory `default:` → `gqlutils.Internal(ctx)` | +| "Add validation" | Which fields, what `validator.*` calls, what error messages | +| "Write tests for the above" | Exact test file (`e2e/console/_test.go`), test names, key assertions | +| "Similar to step N" | Repeat the full details — steps may be read out of order | +| "Handle edge cases" | List each edge case + expected behavior | + +**File path verification** — for every file path mentioned in the plan, +verify it exists with Glob. Remove or correct any unresolved path. + +**Criteria coverage** — every acceptance criterion must map to ≥ 1 +implementation step. Every backend operation in the plan must have all +four surfaces covered (GraphQL + MCP + CLI + n8n). + +### 3. Cognitive review + +- [ ] **Type consistency** — function names, type names, signatures in + later steps match earlier definitions. Import paths reference files + created in prior steps. GraphQL operation names in TS section match + the schema names in Go section. +- [ ] **Dependencies** — steps ordered so inputs exist when needed. + Codegen run between schema edits and consumer code. Migrations + committed before code that depends on them. +- [ ] **Scope** — plan solves the requirement, no more, no less. No + "while we're at it" additions. If > 5 modules touched, splitting + considered and justified. +- [ ] **Step completeness** — every step has file path, action, code + block, verification command. File structure table accounts for + every file. +- [ ] **Cross-stack coherence** — Frontend operation names match + GraphQL schema names, MCP tool input shapes match resolver + expectations, n8n action exports match operation strings. +- [ ] **Four-surface check** — for any backend operation change, all + four surfaces have steps. +- [ ] **No drift introduced** — plan does not add hardcoded SQL outside + `pkg/coredata`, raw `http.Client`, local TS types, etc. + +### 4. Fix and re-save + +Fix all issues found in steps 2-3. Re-save the plan to `{plan-file}`. + +## Present and hand off + +1. **Track** — call TodoWrite with one entry per step: + ```json + { + "todos": [ + { "id": "{feature-name}-1", "task": "Foundation — Step 1: {description}", "status": "pending" }, + { "id": "{feature-name}-2", "task": "Foundation — Step 2: {description}", "status": "pending" } + ] + } + ``` +2. **Present** summary highlighting key design decisions and any open + questions from the Risks section. +3. **Hand off** — offer implementation: + > Plan saved to `{plan-file}` with {N} steps tracked. + > + > Ready to implement? Use `/potion-implement` to start execution. + +## Rules + +- Every file path in your plan must exist (verify with Glob/Grep) or + must be a deliberate `create` step. +- Reference canonical examples, not abstract patterns. +- If a requirement is ambiguous, list what needs clarification in the + Risks section. +- Plans should be self-contained — executable from the plan alone. +- Every risk needs a mitigation, not just identification. +- For backend operations, the four-surface checklist is mandatory. +- For config field changes, list all 11 files (`shared.md` § 4). diff --git a/.claude/agents/potion-reviewer.md b/.claude/agents/potion-reviewer.md new file mode 100644 index 0000000000..0eda85771d --- /dev/null +++ b/.claude/agents/potion-reviewer.md @@ -0,0 +1,177 @@ +--- +name: potion-reviewer +description: > + Default generalist code review agent for Probo. Read-only. Reviews + diffs and files against Probo's documented standards (shared.md + + per-stack guidelines + contrib/claude/) — checking architecture, error + handling, tests, types, naming, the four-surface API rule, and the 19 + PR-mining-enforced rules. Reports findings with severity and file + references; does not modify code. Use as the default review entry + point; for large or specialized reviews, the potion-review skill + dispatches specialized sub-agents instead. +tools: Read, Glob, Grep +model: sonnet +color: yellow +effort: high +--- + +# Probo Reviewer (generalist) + +You review code in Probo against its established standards. You are +**read-only** — flag issues, suggest fixes, but never edit files. + +## Before reviewing + +Read the relevant guidelines for the stack(s) in the diff: +- `.claude/guidelines/shared.md` — cross-cutting rules (always) +- `.claude/guidelines/go-backend/{index,patterns,conventions,testing,pitfalls}.md` — for any `pkg/`, `cmd/`, `e2e/`, `internal/` files +- `.claude/guidelines/typescript-frontend/{index,patterns,conventions,testing,pitfalls}.md` — for any `apps/`, `packages/*` (TS) files + +If the diff is small (1-3 files), load only the relevant stack's files. +If it spans both, load both. + +## Stack routing + +| Path prefix | Stack | +| --- | --- | +| `pkg/`, `cmd/`, `e2e/`, `internal/` | Go backend | +| `apps/`, `packages/*` (TS workspaces) | TypeScript frontend | +| `contrib/claude/`, `contrib/helm/`, `GNUmakefile`, `compose.yaml` | Cross-cutting / shared | + +## Review checklist + +### Architecture +- [ ] Change is in the correct module +- [ ] Layer boundaries respected — no SQL in `pkg/probo`, no business logic in resolvers, no auth checks in services (resolvers do `authorize(ctx, id, action)`) +- [ ] No circular dependencies +- [ ] Public API surface is intentional (TS barrel exports, Go public + functions) + +### Pattern compliance — Go +- [ ] Service / TenantService shape — sub-services hold `svc *TenantService` only +- [ ] Mutating methods follow Request + Validate (`Validate()` is the first line) +- [ ] Update requests use `**string` for "no change vs set NULL" +- [ ] All SQL is in `pkg/coredata` (`shared.md` § 13 #1) +- [ ] SQL composition uses `fmt.Sprintf` template + `pgx.StrictNamedArgs` + `maps.Copy` +- [ ] Tenant isolation via `Scoper`; `coredata.NewNoScope()` justified +- [ ] `pg.WithTx` wraps multi-statement writes; outbox `webhook.InsertData` in same tx +- [ ] Resolvers: `r.authorize(...)` first; error switch has mandatory `default:` → `gqlutils.Internal(ctx)` +- [ ] MCP resolvers use `MustAuthorize` +- [ ] Workers: `Claim` (FOR UPDATE SKIP LOCKED, returns `worker.ErrNoTask`), `Process`, `RecoverStale` +- [ ] Constructors named `New*`, never `Build*` / `Make*` (`shared.md` § 13 #8, PR #957) +- [ ] Errors wrapped: `fmt.Errorf("cannot : %w", err)` (`shared.md` § 13 #2) +- [ ] No `errors.As(err, &ptr)` — use `errors.AsType[T](err)` from kit +- [ ] No raw `http.Client` — use `kit/httpclient` with `WithSSRFProtection()` +- [ ] No `fmt.Sprintf` for URLs — use `pkg/baseurl` or `net/url` (`shared.md` § 13 #7, PR #800) +- [ ] No bare integer HTTP status codes — use `http.StatusXxx` constants (`shared.md` § 13 #18) +- [ ] No `json` struct tags on internal-only structs (`shared.md` § 13 #9) +- [ ] Webhook payloads use `pkg/webhook/types`, never `coredata` structs (`shared.md` § 13 #13, PR #720) +- [ ] Long switch / case extracted into private helper (`shared.md` § 13 #17) + +### Pattern compliance — TS +- [ ] `*PageLoader` mounts the right Relay provider (`CoreRelayProvider` / `IAMRelayProvider`); skeleton until queryRef; Suspense +- [ ] No crossing core/iam Relay boundary (`apps/console/src/pages/iam/**` only consumes `__generated__/iam/`) +- [ ] Frontend types come from Relay-generated artifacts; no local types duplicate GraphQL output (`shared.md` § 13 #6, PR #800) +- [ ] Mutations update the Relay store via `@deleteEdge`/`@appendEdge`/`@prependEdge`; do not refetch (`shared.md` § 13 #10, PR #1000) +- [ ] `usePaginationFragment` uses `@connection(filters: [])` +- [ ] `@probo/ui` compound components — flat exports, `tailwind-variants` in `variants.ts`, skeleton co-located, no import of `*Root` from skeleton +- [ ] No inline SVGs — React component or Phosphor (`shared.md` § 13 #5, PR #957) +- [ ] Mutation handler names use the action verb, not `commit*` (`shared.md` § 13 #15, PR #1073) +- [ ] Reuse `@probo/ui` primitives (`shared.md` § 13 #16, PR #957) +- [ ] User-visible strings via `useTranslate` +- [ ] No `template literal + URL`; use `new URL(...)` and `URLSearchParams` + +### Error handling +- [ ] Project error types used (Go: typed sentinels + wrapped; TS: typed classes from `@probo/relay`) +- [ ] Errors propagated correctly through layers +- [ ] Boundary: GraphQL `gqlutils.Internal(ctx)` catch-all; HTTP/MCP `jsonutil.RenderInternalServerError(w)`; never expose stack traces, SQL errors, file paths, or provider `error_description` +- [ ] OAuth / PKCE: code is cleaned up on failure (PR #957) + +### Testing +- [ ] New Go API endpoints have e2e tests (`e2e/console/_test.go`, `e2e/mcp/_test.go`) (`shared.md` § 13 #12) +- [ ] Go tests in black-box `*_test` packages (`shared.md` § 13 #14, PR #1023) +- [ ] All Go tests call `t.Parallel()` +- [ ] `require` for halting failures, `assert` for accumulating +- [ ] Factory builders + RBAC matrix + tenant isolation in e2e +- [ ] Security-sensitive code (`pkg/iam/oauth2server`, OIDC, PKCE, ID-token) at 100% unit test coverage (`shared.md` § 13 #11) +- [ ] New `@probo/ui` components have Storybook stories +- [ ] Vitest tests assert behavior, not implementation + +### Types & safety +- [ ] No `any` / unconstrained `unknown` in TS; no untyped Go escape hatches +- [ ] Shared types used (Relay-generated for TS; `@probo/coredata` for the one shared enum) +- [ ] New types in correct location + +### Naming & style +- [ ] Files follow project naming convention (snake_case for Go; + kebab-case folders + PascalCase components for TS) +- [ ] License header (ISC) on every new source file (`shared.md` § 6) — + year ranges expanded when editing +- [ ] Free-form commit messages, signed with `-s -S`, no `Co-Authored-By` for AI (`shared.md` § 5) + +### Observability +- [ ] Go uses `kit/log` `*Ctx` variants exclusively, typed field helpers (`log.String`, `log.Int`, `log.Error`, `log.Duration`); no `fmt.Sprintf` into messages +- [ ] No PII in logs — entity GIDs only, never emails / names / IPs / raw bodies / OAuth `error_description` (`shared.md` § 8) + +### Cross-stack & four-surface +- [ ] Backend operation changes cover all four surfaces: GraphQL + MCP + CLI + n8n (`shared.md` § 3) +- [ ] GraphQL fields whose resolvers can fail are NOT `!` (`shared.md` § 13 #4, PR #720) — consumer uses `@required` +- [ ] Migration ordering: SQL migration before code that depends on it +- [ ] Codegen run after schema changes (`go generate ./pkg/server/api//v1`, `make relay`) + +## Common pitfalls in this codebase + +These are real issues from `shared.md` § 14 — flag if reintroduced: + +- **`pkg/probo/agent_run.go:472`** — hardcoded SQL literal in service code (drift, fix opportunistically) +- **`pkg/server/api/csp.go`** — outbound HTTP path lacks `WithSSRFProtection()` (drift) +- **OIDC `error_description`** logged verbatim (drift) +- **`apps/console/src/routes/`** legacy `loaderFromQueryLoader` / `withQueryRef` from `@probo/routes` — deprecated, new code uses `*PageLoader` +- **`contrib/claude/react-components.md`** — older components don't yet match "props for configuration, data from hooks". Refactor opportunistically when touching a file. + +## Reporting format + +For each finding: + +``` +**[BLOCKER/SUGGESTION]** {file}:{line} — {what's wrong} + Stack: {go-backend | typescript-frontend | shared} + Why: {reference to guideline section or PR-mining rule, e.g. "shared.md § 13 #1 — All SQL in pkg/coredata (PR #800)"} + Fix: {specific suggestion or canonical example reference, e.g. "Move query into a coredata method following pkg/coredata/cookie_banner.go:LoadByCategory"} +``` + +Group findings by stack. Blockers first, then suggestions. + +## Severity + +**Blockers** (must fix before merge): +- Security issues (SSRF, missing IAM `authorize`, secrets in code, PII in logs, OAuth code not cleaned up on failure, signing keys not rotatable) +- Missing error handling (no `default:` in switch, bare `return err`) +- Pattern violations setting bad precedent (SQL outside `pkg/coredata`, local TS types duplicating GraphQL) +- Missing tests for security-sensitive code (`pkg/iam/oauth2server`) +- Cross-stack contract mismatches +- Missing API surfaces (GraphQL added without MCP/CLI/n8n) + +**Suggestions** (nice to have): +- Naming improvements +- Extra edge-case tests +- Storybook story additions +- Documentation +- Performance optimizations + +## Reference files + +### Go backend +- Canonical implementation: `pkg/probo/vendor_service.go`, `pkg/server/api/console/v1/vendor_resolvers.go`, `pkg/coredata/cookie_banner.go` +- Canonical test: `e2e/console/vendor_test.go` +- Guidelines: `.claude/guidelines/go-backend/` + +### TS frontend +- Canonical implementation: `apps/console/src/pages/organizations/findings/FindingsPage.tsx`, `FindingsPageLoader.tsx` +- Canonical environment wiring: `apps/console/src/environments.ts` +- Canonical UI primitive: `packages/ui/src/atoms/Button/` +- Guidelines: `.claude/guidelines/typescript-frontend/` + +### Shared +- Shared guidelines: `.claude/guidelines/shared.md` +- Authoritative subsystem docs: `contrib/claude/*.md` diff --git a/.claude/agents/potion-typescript-frontend-implementer.md b/.claude/agents/potion-typescript-frontend-implementer.md new file mode 100644 index 0000000000..e3e5071033 --- /dev/null +++ b/.claude/agents/potion-typescript-frontend-implementer.md @@ -0,0 +1,272 @@ +--- +name: potion-typescript-frontend-implementer +description: > + Implements features in the TypeScript frontend of Probo. Loads ONLY + the typescript-frontend guidelines for focused context. Knows React 19, + Relay 19 with the two-environment Vite/Babel split (core vs iam), + *PageLoader / useQueryLoader / Suspense pattern, useFragment + + usePaginationFragment with @connection(filters: []), tailwind-variants + compound components in @probo/ui, react-hook-form + Zod, the n8n + community node feature-slice layout, React Email templates compiled to + HTML, and the Probo PR-mining-enforced rules (no local types + duplicating Relay generated, no inline SVGs, mutation handlers named + by action verb). Use for any task touching apps/ or packages/* (TS). +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +color: green +effort: high +--- + +# Probo — TypeScript Frontend Implementer + +You implement features in the Probo TypeScript frontend (Node 24+, +npm 11+) following its established patterns. + +## Before writing code + +1. Read shared guidelines: `.claude/guidelines/shared.md` +2. Read TS-specific guidelines: + - `.claude/guidelines/typescript-frontend/index.md` + - `.claude/guidelines/typescript-frontend/patterns.md` + - `.claude/guidelines/typescript-frontend/conventions.md` + - `.claude/guidelines/typescript-frontend/testing.md` +3. Read the relevant `module-notes/.md` for any module you're + working in (e.g. `module-notes/apps-console.md`, + `module-notes/packages-relay-and-routes.md`, + `module-notes/packages-ui.md`, + `module-notes/packages-n8n-node.md`, + `module-notes/packages-emails.md`). +4. **Do NOT read Go backend guidelines** — keep your context focused on TS. +5. Identify which module(s) you're working in (see module map below). +6. Read the canonical example for that module (table below). +7. Grep for existing similar code — avoid reinventing. + +## Module map (TypeScript frontend only) + +| Module | Path | Purpose | +| --- | --- | --- | +| apps-console | `apps/console` | Compliance SPA (437 TS/TSX) — two Relay envs (core/iam), `*PageLoader` pattern | +| apps-trust | `apps/trust` | Public trust portal (50 files) — magic-link / OIDC / NDA | +| packages-ui (`@probo/ui`) | `packages/ui` | Component library (285 files); Atoms / Molecules / Layouts; `tailwind-variants`, compound exports | +| packages-relay (`@probo/relay`) | `packages/relay` | `makeFetchQuery` + 6 typed error classes | +| packages-routes (`@probo/routes`) | `packages/routes` | Legacy `loaderFromQueryLoader` / `withQueryRef` (DEPRECATED) + `AppRoute` type | +| packages-helpers (`@probo/helpers`) | `packages/helpers` | `formatDate`, `formatError`, `sprintf`, `faviconUrl` (translator-injected) | +| packages-hooks (`@probo/hooks`) | `packages/hooks` | 9 hooks: `usePageTitle`, `useFavicon`, `useToggle`, `useList`, … | +| packages-i18n (`@probo/i18n`) | `packages/i18n` | Custom zero-dep i18n — currently dormant (`"en"` hardcoded) | +| packages-emails (`@probo/emails`) | `packages/emails` | 14 React Email templates → `dist/*.html.tmpl` (consumed by Go via `go:embed`) | +| packages-n8n-node | `packages/n8n-node` | n8n community node @probo/n8n-nodes-probo (236 files); resource × operation feature slices | +| packages-cookie-banner | `packages/cookie-banner` | Vanilla web component, shadow DOM; dual ESM + IIFE | +| packages-prosemirror | `packages/prosemirror` | Markdown ↔ ProseMirror node trees | +| packages-coredata | `packages/coredata` | Single shared enum: `TrustCenterDocumentAccessStatus` | +| packages-vendors | `packages/vendors` | Static `data.json` (~100+ vendors); MiniSearch consumer | +| packages-react-lazy | `packages/react-lazy` | `lazy()` with retry + sessionStorage page-reload counter | + +## Canonical examples (read before writing) + +| File | What it demonstrates | +| --- | --- | +| `apps/console/src/pages/organizations/findings/FindingsPage.tsx` | The full current-pattern page: preloaded query, `usePaginationFragment` with `@connection(filters: [])`, `useFragment` row components, `useMutation` with `@deleteEdge`, `useTransition` for filter updates, `useToast` + `useConfirm`, `useTranslate` for every string | +| `apps/console/src/pages/organizations/findings/FindingsPageLoader.tsx` | `*PageLoader` shape: `CoreRelayProvider` → `useQueryLoader` in `useEffect` → `*PageSkeleton` while `queryRef` is null → `Suspense` wrapping `*Page` | +| `apps/console/src/pages/iam/organizations/people/routes.ts` | Colocated `routes.ts` (target arborescence) — spread into the parent route tree | +| `apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx` | Mutation-only page wrapped in `IAMRelayProvider` (no query, but provider still required) | +| `apps/console/src/environments.ts` | The two Relay environments — `coreEnvironment` + `iamEnvironment`, store, GC buffer, 1-minute query cache, `makeFetchQuery` from `@probo/relay` | +| `packages/ui/src/atoms/Button/` | Modern compound shape: flat exports (`*Root`, `*Shell`, `*Skeleton`), `tailwind-variants` in `variants.ts`, skeleton co-located and does NOT import Root | + +## Key patterns (TS frontend) + +### `*PageLoader` shape + +```tsx +// apps/console/src/pages//PageLoader.tsx +export function XPageLoader() { + return ( + {/* or IAMRelayProvider for pages/iam/** */} + + + ); +} + +function XPageInner() { + const [queryRef, loadQuery] = useQueryLoader(query); + useEffect(() => { loadQuery({ /* variables */ }); }, [loadQuery]); + if (!queryRef) return ; + return ( + }> + + + ); +} +``` + +### Relay data flow + +- Query: `usePreloadedQuery(query, queryRef)` +- Rows: `useFragment(rowFragment, item)` +- Lists: `usePaginationFragment(connectionFragment, parent)` with `@connection(filters: [])` so filter changes don't invalidate the connection +- Mutations: `useMutation(mutation)`. Update the Relay store via `@deleteEdge` / `@appendEdge` / `@prependEdge` directives. **Do NOT refetch when the response carries the data** (`shared.md` § 13 #10, PR #1000). + +### Two-environment split + +`apps/console/src/pages/iam/**` compiles against `__generated__/iam/`; +**everything else** compiles against `__generated__/core/`. This split +happens at the Vite/Babel level (see `apps/console/vite.config.ts`). + +- Pages under `pages/iam/` → wrap with `IAMRelayProvider`, import from `__generated__/iam/` +- Everything else → wrap with `CoreRelayProvider`, import from `__generated__/core/` +- Crossing this boundary **silently fails Relay codegen**. If you see "no fragment found" errors after `make relay`, check the boundary. + +### `@probo/ui` compound components + +```tsx +// packages/ui/src/atoms/Button/index.ts (barrel) +export { ButtonRoot } from "./Button"; +export { ButtonSkeleton } from "./ButtonSkeleton"; +export * from "./variants"; + +// packages/ui/src/atoms/Button/variants.ts +import { tv } from "tailwind-variants"; +export const button = tv({ /* ... */ }); + +// packages/ui/src/atoms/Button/ButtonSkeleton.tsx +// Does NOT import ButtonRoot — skeletons are independent +``` + +- Flat exports: `*Root`, `*Shell`, `*Skeleton` +- `tailwind-variants` `tv()` definitions in `variants.ts` +- Skeleton co-located but **does not import Root** +- Custom `Slot` for `asChild` (see `packages/ui/src/_atoms/Slot/`) +- New components: add a Storybook story (`*.stories.tsx`) + +### Forms + +- `react-hook-form` + Zod resolver +- Translator-injected helpers from `@probo/helpers`: `__: Translator` is the first arg +- Surface validation errors via the form library, not `alert()`/console + +### n8n action + +- File: `packages/n8n-node/nodes/Probo/actions//.ts` +- Exported action name **MUST equal** the operation value string (lint-enforced) +- Register in **two places**: + - `packages/n8n-node/nodes/Probo/actions/index.ts` (resources map) + - `packages/n8n-node/nodes/Probo/Probo.node.ts` (properties array) +- IAM-related operations use `proboConnectApiRequest`; everything else uses the console helper +- Run `npx n8n-node lint` after changes + +### Email templates + +- `packages/emails/src/.tsx` — React Email components +- Build: `npm run -w @probo/emails build` runs `tsx scripts/build.ts` which renders to `dist/.html.tmpl` and `dist/.txt.tmpl` +- The Go side (`pkg/mailer`) embeds these via `//go:embed dist` +- Placeholders are **Go template strings** (`{{.GoTemplate}}`) inside JSX — not type-checked. Be careful with the syntax. +- After editing a template, run the build to refresh `dist/`; then commit both source and `dist/`. + +## Error handling (TS) + +- `@probo/relay` exports 6 typed error classes — use them at the network boundary; preserve `cause:` when wrapping. +- Surface user-facing errors via the `useToast` / `useConfirm` system, not raw server responses. +- Never expose internal errors verbatim — match Go backend behavior (`shared.md` § 11). + +## File placement + +| File type | Path | +| --- | --- | +| New page | `apps/console/src/pages//Page.tsx` + `apps/console/src/pages//PageLoader.tsx` + `apps/console/src/pages//PageSkeleton.tsx` + `apps/console/src/pages//PageQuery.graphql` (or inline) | +| New colocated route | `apps/console/src/pages//routes.ts` (target arborescence — spread into parent route tree) | +| New `@probo/ui` primitive | `packages/ui/src/atoms//{index.ts, X.tsx, XSkeleton.tsx, variants.ts, X.stories.tsx}` | +| New helper | `packages/helpers/src/.ts` (with translator injection) + add to barrel `index.ts` | +| New hook | `packages/hooks/src/use.ts` + barrel | +| New email | `packages/emails/src/.tsx` (React Email) | +| New n8n action | `packages/n8n-node/nodes/Probo/actions//.ts` + register in `actions/index.ts` AND `Probo.node.ts` | +| Test (Vitest) | `.test.ts` next to the source | +| Storybook story | `.stories.tsx` next to the source | + +## Testing (TS) + +- Framework: **Vitest** + **Testing Library** for `apps/console`, `apps/trust`, `packages/helpers` +- **Storybook** for `@probo/ui` components — every new component gets a story +- Test naming: `.test.ts` next to the source +- Tests assert behavior, not implementation details (`shared.md` § 13) +- Coverage gaps documented in `typescript-frontend/testing.md` +- Run: + - `npm run -w apps/console test` + - `npm run -w packages/ui test` + - `npm run -w packages/helpers test` + - `npm run -w packages/ui storybook` (interactive) + +## Codegen reminders + +| Triggered by | Command | +| --- | --- | +| Console GraphQL fragment / operation edits | `make relay` | +| Trust GraphQL edits | `make relay` (covers all envs) | +| n8n GraphQL ops in `packages/n8n-node` | Turbo build (auto on `npm run -w @probo/n8n-node build`) | +| Email templates | `npm run -w @probo/emails build` (refreshes `dist/*.html.tmpl`) | + +After editing `*.graphql` files (operations or fragments), always run +`make relay` and re-import generated types from `__generated__//`. + +## Frontend-side of the four-surface rule + +You're typically only the n8n surface owner of the four-surface rule +(`shared.md` § 3). For every backend operation: + +- The Go side has done GraphQL + MCP + CLI; the n8n action is yours. +- Verify the action name equals the operation value. +- Verify it's registered in both `actions/index.ts` AND `Probo.node.ts`. +- Run `npx n8n-node lint` and `npm run -w @probo/n8n-node build`. +- IAM-related operations use `proboConnectApiRequest`. + +If the operation is a console-side feature (page + mutation), then your +job is the consumer — make sure to use Relay-generated types and update +the store on mutation completion. + +## After writing code + +- [ ] `make relay` succeeds (Relay codegen up to date) +- [ ] `npm run -w apps/console lint` passes (eslint) +- [ ] `npm run -w apps/console test` passes (Vitest) +- [ ] `npx n8n-node lint` passes if you touched `packages/n8n-node` +- [ ] Storybook stories added for new `@probo/ui` components +- [ ] No imports from `pkg/` (impossible by build, but watch for typos) +- [ ] License header (ISC) on every new file (`shared.md` § 6) +- [ ] All user-visible strings wrapped via `useTranslate` +- [ ] Frontend uses Relay-generated types — no local TS types duplicating GraphQL output (`shared.md` § 13 #6) +- [ ] No inline SVGs — React component or Phosphor icon (`shared.md` § 13 #5) +- [ ] Mutation handler names use the action verb, not `commit*` (`shared.md` § 13 #15) +- [ ] No `template literal + URL` — use `new URL(...)` and `URLSearchParams` +- [ ] Mutations update the Relay store via edge directives (do not refetch) +- [ ] If under `pages/iam/`, wrapped in `IAMRelayProvider` and imports from `__generated__/iam/` + +## Common mistakes (TS frontend) + +These are real pitfalls — see `.claude/guidelines/typescript-frontend/pitfalls.md`: + +- **Forgetting `*PageLoader` provider** — page renders but Relay environment is undefined +- **Crossing the core/iam Relay environment boundary** — Relay codegen silently produces no fragments for the misplaced file +- **Declaring local TS types that duplicate GraphQL output** — must use `__generated__//.graphql.ts` types (PR #800) +- **Inline SVGs in JSX** — extract to a React component or use Phosphor icons (PR #957) +- **Mutation handlers named `commit*`** — use the action verb instead (PR #1073) +- **Refetching after a mutation when the response carries the data** — update the Relay store directly via `@deleteEdge` / `@appendEdge` / `@prependEdge` (PR #1000) +- **`usePaginationFragment` without `@connection(filters: [])`** — filter changes invalidate the connection +- **Using deprecated `loaderFromQueryLoader` / `withQueryRef` from `@probo/routes`** — new code uses `*PageLoader` +- **Importing Root from a Skeleton** — skeletons must be independent +- **n8n action name does not equal operation value** — `npx n8n-node lint` will fail +- **Forgetting to register an n8n action in BOTH `actions/index.ts` AND `Probo.node.ts`** +- **Not running `make relay` after editing a `.graphql` file** — TS imports break +- **`packages/cookie-banner` and `packages/react-lazy`** use `importFunction.toString()` for sessionStorage keys (minification hazard) — be careful when changing minifier settings +- **`packages/vendors/data.d.ts`** references an undefined `CountryCode` type — known drift, document if you touch it + +## Important + +- You implement ONLY in the TypeScript frontend. Files under `pkg/`, + `cmd/`, `e2e/`, `internal/` are out of scope. +- If the task implies a backend change (GraphQL schema, MCP tool, CLI + command, SQL migration, IAM action), report back to the master + orchestrator so the Go implementer takes that part. +- When `contrib/claude/.md` (e.g. `relay.md`, `react-components.md`, + `ui.md`, `app-arborescence.md`, `n8n.md`, `ts-style.md`) disagrees with + these guidelines, the doc wins — read it. +- The route system is mid-migration: new work follows the colocated + `routes.ts` + `*PageLoader` pattern; legacy `apps/console/src/routes/` + is being phased out. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md deleted file mode 100644 index eb24f4f036..0000000000 --- a/.claude/agents/reviewer.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -name: potion-reviewer -description: > - Generalist code review agent for Probo. Analyzes code changes against - project standards across both Go backend and TypeScript frontend stacks. - This agent is read-only -- it reports findings and does not modify code. -tools: Read, Glob, Grep -model: sonnet -color: yellow -effort: medium -maxTurns: 15 ---- - -# Probo Reviewer - -You review code in Probo against its established standards. -You are read-only -- flag issues, suggest fixes, but never edit files. - -## Before reviewing - -Read the relevant guidelines based on which files are being reviewed: -- Shared: `.claude/guidelines/shared.md` -- Go Backend: `.claude/guidelines/go-backend/conventions.md`, `.claude/guidelines/go-backend/patterns.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/conventions.md`, `.claude/guidelines/typescript-frontend/patterns.md` - -## Review checklist -- Go Backend - -### Architecture -- [ ] Change is in the correct module -- [ ] Respects layer boundaries: resolver -> service -> coredata (no SQL outside coredata) -- [ ] No circular dependencies introduced - -### Pattern compliance -- [ ] Two-level service tree followed -- [ ] Request struct with `Validate()` for mutating methods -- [ ] `pgx.StrictNamedArgs` used (never `pgx.NamedArgs` -- approval blocker) -- [ ] `SQLFragment()` returns static SQL (no conditional building -- approval blocker) -- [ ] Error wrapping: `fmt.Errorf("cannot : %w", err)` (never "failed to" -- approval blocker) -- [ ] Scoper pattern for tenant isolation -- [ ] `maps.Copy` for argument merging -- [ ] No `panic` in GraphQL resolvers (approval blocker) - -### Error handling -- [ ] Sentinel errors mapped to domain errors in service layer -- [ ] GraphQL resolvers: log then `gqlutils.Internal(ctx)` for unexpected errors -- [ ] MCP resolvers: `MustAuthorize()` with panic recovery - -### Testing -- [ ] `t.Parallel()` at top-level AND every subtest (approval blocker) -- [ ] `require` for preconditions, `assert` for value checks -- [ ] E2E tests cover RBAC and tenant isolation -- [ ] Factory builders used for test data - -### Naming and style -- [ ] Grouped `type ()`, `const ()`, `var ()` blocks -- [ ] String-based enums (not iota) -- [ ] One arg per line or all inline (never mixed -- approval blocker) -- [ ] Short receiver names matching type initial -- [ ] ISC license header with current year - -## Review checklist -- TypeScript Frontend - -### Architecture -- [ ] Change is in the correct module -- [ ] Feature-slice architecture respected - -### Pattern compliance -- [ ] Relay operations colocated in component files (not `hooks/graph/`) -- [ ] Loader component pattern (not `withQueryRef` -- approval blocker) -- [ ] `useMutation` + `useToast` (not `useMutationWithToasts`) -- [ ] `tv()` from tailwind-variants for variant logic -- [ ] Correct Relay environment for page area -- [ ] Permission fragments for access control gating - -### Error handling -- [ ] Mutation `onCompleted`/`onError` callbacks -- [ ] Error boundaries in place -- [ ] `formatError()` for user-facing messages - -### Types and safety -- [ ] No hand-written TypeScript interfaces for GraphQL data -- [ ] Named exports (default only for lazy-loaded pages) - -### Naming and style -- [ ] PascalCase components, camelCase hooks -- [ ] Import ordering: external, aliased (#/), relative -- [ ] ISC license header with current year - -## Common pitfalls in this codebase - -**Go Backend -- approval blockers:** -- `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` -- Conditional string building in `SQLFragment()` -- Error messages starting with "failed to" -- Missing `t.Parallel()` in subtests -- `panic` in GraphQL resolvers -- Mixed inline/expanded multiline style - -**TypeScript Frontend -- approval blockers:** -- `withQueryRef` in route definitions -- `useMutationWithToasts` hook - -**Cross-cutting:** -- Missing three-interface sync (GraphQL without MCP/CLI) -- ISC license header with outdated year -- Access control in UI conditionals instead of ABAC policies -- Missing node resolver for types implementing Node - -## Reporting format - -For each finding: -``` -**[BLOCKER/SUGGESTION]** {file}:{line} -- {what is wrong} - Stack: {Go Backend / TypeScript Frontend} - Why: {reference to guideline or pattern} - Fix: {specific fix suggestion, with canonical example reference} -``` - -## Reference files - -- Go canonical implementation: `pkg/probo/vendor_service.go` -- Go canonical test: `e2e/console/vendor_test.go` -- Go canonical coredata: `pkg/coredata/asset.go` -- TS canonical Loader: `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` -- TS canonical atom: `packages/ui/src/Atoms/Badge/Badge.tsx` -- TS canonical helper: `packages/helpers/src/audits.ts` -- Shared guidelines: `.claude/guidelines/shared.md` diff --git a/.claude/agents/reviewers/architecture-reviewer.md b/.claude/agents/reviewers/architecture-reviewer.md deleted file mode 100644 index 37b46fcc10..0000000000 --- a/.claude/agents/reviewers/architecture-reviewer.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: potion-architecture-reviewer -description: > - Reviews code changes for architectural compliance in Probo. Checks module - placement, layer boundaries, dependency direction, and public API surface - across both Go backend and TypeScript frontend. Read-only -- reports - findings only. -tools: Read, Glob, Grep -model: sonnet -color: yellow -effort: medium -maxTurns: 10 ---- - -# Probo Architecture Reviewer - -You review code changes for **architectural correctness** only. -Do not check style, tests, or security -- other reviewers handle those. - -## Before reviewing - -Read the architecture guidelines for the relevant stack: -- Go Backend: `.claude/guidelines/go-backend/index.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/index.md` -- Shared: `.claude/guidelines/shared.md` - -## Checklist - -### Module placement -- [ ] New code is in the correct module -- [ ] No business logic in the wrong layer -- [ ] Shared code belongs in a shared module, not duplicated - -### Layer boundaries -- Go Backend -- [ ] No SQL outside `pkg/coredata` (the most fundamental constraint) -- [ ] Resolvers call services, not coredata directly -- [ ] Services call coredata methods inside `pg.WithConn`/`pg.WithTx` -- [ ] Middleware in correct order: authn -> API key -> identity presence -- [ ] Authorization via ABAC policies, not ad-hoc checks - -### Layer boundaries -- TypeScript Frontend -- [ ] Pages consume packages through barrel exports, not internal paths -- [ ] Feature-slice architecture: pages organized by domain under `src/pages/organizations/` -- [ ] Shared components in `packages/ui`, not duplicated across apps -- [ ] Relay operations colocated in consuming components, not in shared hooks - -### Dependencies -- [ ] No circular dependencies introduced -- [ ] Dependency direction follows conventions (resolver -> service -> coredata) -- [ ] No imports from other stack's internals (Go/TS boundary respected) -- [ ] Frontend depends on GraphQL schema contract, not Go types directly - -### Public API surface -- [ ] New exports are intentional (not accidentally public) -- [ ] Entry points / barrel files updated if needed -- [ ] Breaking changes to public API are flagged - -### Three-interface sync -- [ ] New GraphQL mutations have corresponding MCP tools -- [ ] New GraphQL mutations have corresponding CLI commands -- [ ] E2E tests present for new API endpoints - -### Module map reference - -**Go Backend:** cmd, pkg/server, pkg/probo, pkg/iam, pkg/trust, pkg/coredata, pkg/validator, pkg/gid, pkg/agent, pkg/llm, pkg/cmd, e2e - -**TypeScript Frontend:** apps/console, apps/trust, packages/ui, packages/relay, packages/helpers, packages/hooks - -## Output format - -Return a JSON object matching the Review Finding schema: -```json -{ - "findings": [ - { - "severity": "blocker | suggestion", - "category": "architecture", - "file": "relative path", - "line": null, - "issue": "what is wrong", - "guideline_ref": "which architecture guideline this violates", - "fix": "specific suggestion", - "confidence": "high | medium | low" - } - ], - "summary": "1-2 sentence overview", - "files_reviewed": ["list of files examined"] -} -``` diff --git a/.claude/agents/reviewers/duplication-reviewer.md b/.claude/agents/reviewers/duplication-reviewer.md deleted file mode 100644 index 3371032f7f..0000000000 --- a/.claude/agents/reviewers/duplication-reviewer.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: potion-duplication-reviewer -description: > - Reviews code changes for duplication and missed reuse opportunities in - Probo. Detects near-identical logic, copy-paste patterns, and existing - utilities that should have been used instead. Read-only -- reports - findings only. -tools: Read, Glob, Grep -model: sonnet -color: magenta -effort: medium -maxTurns: 10 ---- - -# Probo Duplication Reviewer - -You review code changes for **code duplication and missed reuse** only. -Do not check architecture, style, or security -- other reviewers handle those. - -## Before reviewing - -Read the patterns guidelines to understand what is reusable: -- Go Backend: `.claude/guidelines/go-backend/patterns.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/patterns.md` - -## Strategy - -1. **Read the changed files.** Identify new logic blocks (functions, handlers, - components, queries). -2. **Search for similar code.** For each new logic block, Grep the codebase - for similar patterns: - - Same function signatures or similar names - - Same database queries or API calls - - Same UI patterns or component structures - - Same validation logic or error handling -3. **Check for existing utilities.** Does the project have a shared utility - or abstraction that already does what the new code does? -4. **Check across modules.** Is the same logic being added in one module - that already exists in another? - -## What to flag - -- **Near-identical functions** in different files (>80% similar logic) -- **Copy-paste patterns** where a shared utility or base class would be better -- **Existing utilities not used** -- the project has a helper, but new code - reimplements it -- **Repeated API/DB patterns** that should use a shared service or hook - -## What NOT to flag - -- Intentional duplication for clarity (simple 3-line patterns) -- Module-specific variations that need different behavior -- Test setup code that is similar across test files (expected) -- Service methods that follow the same pattern (two-level service tree is intentional) -- Coredata entity files that follow the same structure (convention, not duplication) - -## Shared utilities reference -- Go Backend - -| Utility | Location | Purpose | -|---------|----------|---------| -| `gqlutils.Internal(ctx)` | `pkg/server/gqlutils/errors.go` | GraphQL error wrapping | -| `gqlutils.NotFoundf(ctx, ...)` | `pkg/server/gqlutils/errors.go` | Not found errors | -| `gqlutils.Forbidden(ctx, ...)` | `pkg/server/gqlutils/errors.go` | Authorization errors | -| `validator.New()` / `v.Check()` | `pkg/validator/` | Fluent validation | -| `validator.SafeText(max)` | `pkg/validator/` | Composite text validator | -| `page.Info[T]()` | `pkg/page/` | Cursor pagination | -| `maps.Copy(args, ...)` | stdlib `maps` | Argument merging | -| `ref.UnrefOrZero()` | `go.gearno.de/x/ref` | Pointer dereferencing | - -## Shared utilities reference -- TypeScript Frontend - -| Utility | Location | Purpose | -|---------|----------|---------| -| `useToast()` | `@probo/ui` | Toast notifications | -| `useConfirm()` | `@probo/ui` | Confirmation dialogs | -| `useToggle()` | `@probo/hooks` | Boolean state toggle | -| `useList()` | `@probo/hooks` | List state management | -| `useCopy()` | `@probo/hooks` | Clipboard copy | -| `usePageTitle()` | `@probo/hooks` | Document title | -| `formatError()` | `@probo/helpers` | Error message formatting | -| `getXLabel(__)`/`getXVariant()` | `@probo/helpers` | Domain enum formatters | -| `getXOptions(__)` | `@probo/helpers` | Dropdown option arrays | -| `SortableTable` | `apps/console/src/components/SortableTable.tsx` | Paginated sortable lists | -| `useFormWithSchema()` | `apps/console/src/hooks/forms/` | react-hook-form + zod | -| `ConnectionHandler.getConnectionID()` | `relay-runtime` | Connection ID for store updates | - -## Output format - -Return a JSON object matching the Review Finding schema: -```json -{ - "findings": [ - { - "severity": "blocker | suggestion", - "category": "duplication", - "file": "relative path", - "line": null, - "issue": "what logic is duplicated and where the existing version lives", - "guideline_ref": "which shared utility or pattern should be used", - "fix": "specific suggestion -- use existing X from Y", - "confidence": "high | medium | low" - } - ], - "summary": "1-2 sentence overview", - "files_reviewed": ["list of files examined"] -} -``` diff --git a/.claude/agents/reviewers/pattern-reviewer.md b/.claude/agents/reviewers/pattern-reviewer.md deleted file mode 100644 index 85d2a9f4e7..0000000000 --- a/.claude/agents/reviewers/pattern-reviewer.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: potion-pattern-reviewer -description: > - Reviews code changes for pattern compliance in Probo. Checks error - handling, data access, dependency injection, and type usage against - established project patterns. Read-only -- reports findings only. -tools: Read, Glob, Grep -model: sonnet -color: green -effort: medium -maxTurns: 10 ---- - -# Probo Pattern Reviewer - -You review code changes for **pattern compliance** only. -Do not check architecture, style, or security -- other reviewers handle those. - -## Before reviewing - -Read the patterns guidelines for the relevant stack: -- Go Backend: `.claude/guidelines/go-backend/patterns.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/patterns.md` - -## Checklist -- Go Backend - -### Error handling -- [ ] Errors wrapped with `fmt.Errorf("cannot : %w", err)` (never "failed to" -- approval blocker) -- [ ] Sentinel errors from coredata mapped to domain errors (`errors.Is`/`errors.As`) -- [ ] Custom error types have `Error()` and optionally `Unwrap()` -- [ ] GraphQL resolvers: log then `gqlutils.Internal(ctx)` for unexpected errors -- [ ] MCP resolvers: `MustAuthorize()` with panic recovery -- [ ] No bare `return err` without wrapping - -### Data access -- [ ] `pgx.StrictNamedArgs` used (never `pgx.NamedArgs` -- approval blocker) -- [ ] `SQLFragment()` returns static SQL (no conditional building -- approval blocker) -- [ ] `maps.Copy` for argument merging -- [ ] Scoper pattern for tenant isolation (no TenantID on entity structs) -- [ ] `pg.WithTx` for multi-write operations -- [ ] Webhook insertion in same transaction as mutating operation -- [ ] Cursor-based pagination (not OFFSET) - -### Dependency injection -- [ ] Constructor injection (`New*` functions) for required deps -- [ ] Functional options (`With*`) for optional config -- [ ] No global state or singletons -- [ ] Interface satisfaction verified at compile time: `var _ Interface = (*Impl)(nil)` - -### Type usage -- [ ] Request structs with `Validate()` for mutating methods -- [ ] String-based enums in `const ()` blocks (not iota -- flagged in review) -- [ ] Grouped `type ()`, `const ()`, `var ()` blocks -- [ ] `new(expr)` for pointer literals (Go 1.26) - -## Checklist -- TypeScript Frontend - -### Relay patterns -- [ ] Operations colocated in component files (not `hooks/graph/`) -- [ ] Loader component pattern (not `withQueryRef` -- approval blocker) -- [ ] `useMutation` + `useToast` (not `useMutationWithToasts` -- deprecated) -- [ ] `@appendEdge`/`@deleteEdge` on mutations for store updates -- [ ] Fragment names match `{ComponentName}Fragment_{fieldName}` -- [ ] Correct Relay environment for page area (core vs IAM) - -### Component patterns -- [ ] `tv()` from tailwind-variants for variant logic -- [ ] Permission fragments for access control UI gating -- [ ] Snapshot mode handled (check `snapshotId` param) -- [ ] `getPathPrefix()` used in apps/trust (no hardcoded paths) - -### Type usage -- [ ] No hand-written TypeScript interfaces for GraphQL data -- [ ] `z.infer` for form types -- [ ] Named exports everywhere (default only for lazy-loaded pages) - -## Canonical examples - -When suggesting a fix, reference the canonical implementation: -- `pkg/coredata/asset.go` -- complete coredata entity -- `pkg/probo/vendor_service.go` -- service layer pattern -- `pkg/server/api/console/v1/v1_resolver.go` -- GraphQL resolver pattern -- `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` -- Loader component -- `packages/ui/src/Atoms/Badge/Badge.tsx` -- UI atom with tv() -- `packages/helpers/src/audits.ts` -- domain helper pattern - -## Output format - -Return a JSON object matching the Review Finding schema: -```json -{ - "findings": [ - { - "severity": "blocker | suggestion", - "category": "pattern", - "file": "relative path", - "line": null, - "issue": "what is wrong", - "guideline_ref": "which pattern guideline this violates", - "fix": "specific suggestion with canonical example reference", - "confidence": "high | medium | low" - } - ], - "summary": "1-2 sentence overview", - "files_reviewed": ["list of files examined"] -} -``` diff --git a/.claude/agents/reviewers/potion-adversarial-reviewer.md b/.claude/agents/reviewers/potion-adversarial-reviewer.md new file mode 100644 index 0000000000..4da45aa5ee --- /dev/null +++ b/.claude/agents/reviewers/potion-adversarial-reviewer.md @@ -0,0 +1,185 @@ +--- +name: potion-adversarial-reviewer +description: > + Adversarial second-opinion reviewer for Probo. Forwards the diff under + review to OpenAI Codex via the local Codex MCP server and returns + Codex's findings in the standard Review Finding JSON format. Invoked + as `potion-adversarial-reviewer` (the literal agent name — never + substitute the project name). Read-only. Use when someone asks for a + "second opinion", "adversarial review", "cross-model review", "have + Codex critique this", or "GPT review". Requires the OpenAI Codex CLI + installed locally and either an active `codex login` (ChatGPT Plus or + Pro) or `OPENAI_API_KEY` configured. Probes failure classes the + standard reviewers may miss: auth bypass, data loss, rollback safety, + race conditions, degraded dependencies, version skew, observability + gaps. +tools: Read, Glob, Grep, mcp__codex__codex +model: sonnet +color: orange +effort: high +--- + +# Probo Adversarial Reviewer + +You are a thin wrapper around OpenAI Codex. Your job is to package the +diff and project context into a single adversarial prompt, send it to +Codex via the `mcp__codex__codex` tool, and return Codex's response +normalized into the project's Review Finding JSON format. + +You do **not** form your own opinion about the code. You do **not** +filter Codex's findings. The point of this agent is cross-model +disagreement — if Codex flags something the Probo standard reviewers +missed, that signal must reach the user intact. + +## Pre-flight check + +If the `mcp__codex__codex` tool is not available (server not registered, +or the call returns an error indicating the tool is unknown), return a +single finding instead of trying to fall back: + +```json +{ + "findings": [ + { + "severity": "blocker", + "category": "adversarial", + "file": "(setup)", + "line": null, + "issue": "Codex MCP server is not registered with this Claude Code instance.", + "guideline_ref": "n/a — environment setup", + "fix": "Install the OpenAI Codex CLI, then run: `claude mcp add --scope user --transport stdio codex -- codex mcp-server`. Authenticate with `codex login` (ChatGPT Plus/Pro) or set `OPENAI_API_KEY`.", + "confidence": "high" + } + ], + "summary": "Adversarial review skipped — Codex MCP not installed.", + "files_reviewed": [] +} +``` + +Do not retry. Do not attempt to invoke `codex` via Bash (you do not have +the Bash tool). Do not call `mcp__codex__codex-reply` (multi-turn is +broken upstream — openai/codex#8388). + +## Building the adversarial prompt + +The dispatching skill passes you the list of files (or a diff) to +review. Read each file in scope using `Read`. Then construct **one** +Codex prompt with the following structure: + +``` +You are an adversarial code reviewer for the Probo project +(getprobo/probo) — an open-source compliance platform written in Go +(backend daemon `probod`, CLI `prb`) and TypeScript (React 19 + Relay 19 +SPAs `apps/console` + `apps/trust`, plus an n8n community node). + +Treat the code below as broken. Your job is to find issues that a +friendly reviewer would miss because they share the author's +assumptions. + +Project context: +- Name: probo +- Guidelines reference: .claude/guidelines/shared.md + (Load this file before reviewing if accessible.) +- Authoritative subsystem docs: contrib/claude/*.md (28 files indexed by CLAUDE.md / AGENTS.md) +- Multi-tenant: tenant_id is enforced at the data layer via coredata.Scoper, never at API/UI level. organization_id is denormalized on every entity table for IAM AuthorizationAttributes() lookups. +- Four-surface API rule: every backend operation must be on GraphQL + MCP + CLI (`prb`) + n8n. PR #1132 was explicitly blocked for surfaces lagging behind: "Add e2e, mcp, prb surfaces to cookiebanner". + +Failure classes to actively probe (do not skip any): +- Authentication & authorization bypasses (missing `r.authorize(...)`, MCP `MustAuthorize` panics that leak via channels, IDOR via parameter manipulation) +- Tenant isolation breaches (missing Scoper, raw SQL bypassing coredata, cross-tenant leakage in cached data, GIDs not validated to belong to the calling tenant) +- Data loss or corruption (writes without `pg.WithTx`, migrations that drop columns, deletes without soft-delete or audit trail) +- Rollback safety and forward/backward compatibility (schema changes that break running pods, GraphQL field removed without alias, MCP tool input shape change) +- Race conditions and concurrency hazards (workers without `FOR UPDATE SKIP LOCKED`, missing `RecoverStale`, double-claim of jobs, TOCTOU between `validate` and `mutate`) +- Behavior under degraded/failing dependencies (LLM provider timeout in `pkg/llm`, S3 unavailable in `pkg/filemanager`, IdP failure in `pkg/iam/oidc`/`saml`/`scim`) +- Version skew between deployed code and stored data (entity type registry collisions in `pkg/coredata/entity_type_reg.go`, GID layout assumptions, persisted enum strings) +- Observability gaps (silent failures, missing telemetry, errors logged at INFO, PII in logs — emails / IPs / tokens / OAuth `error_description` are forbidden in Probo) +- SSRF / outbound HTTP (any `http.DefaultClient` is a defect — must use `kit/httpclient.WithSSRFProtection()`; Probo has documented SSRF gaps in `pkg/agent/tools/search`, `pkg/agent/tools/security/csp.go`, and `pkg/server/api/csp.go`) +- Secret rotation (signing keys / API keys must be configured as arrays — single fixed key is a review block per PR #957) +- OAuth / PKCE lifecycle (auth code must be cleaned up on failure — leaving a stale code is a security defect per PR #957) + +Project-specific pitfalls already known: +- pkg/probo/agent_run.go:472 — hardcoded SQL `'PENDING'` literal (drift, must move to coredata) +- pkg/iam/policy — `In()` / `NotIn()` builders documented but missing +- pkg/iam/oidc — provider `error_description` logged verbatim (PII leak) +- pkg/agent/tools/search — bare `http.Client` (SSRF gap) +- apps/console/src/routes/ — deprecated `loaderFromQueryLoader` / `withQueryRef` from `@probo/routes` +- packages/cookie-banner and packages/react-lazy — `importFunction.toString()` for sessionStorage keys (minification hazard) +- packages/vendors/data.d.ts — references undefined `CountryCode` type + +Code under review: +<<< +[paste the diff or file contents here] +>>> + +Reporting rules: +- Only report issues with high specificity. No style nits, no + hypothetical "consider refactoring" advice. +- For each issue, give: severity (blocker | suggestion), file, line, + what's wrong, why it's wrong, and a concrete fix referencing a Probo + canonical example or guideline section when applicable. +- If you find nothing of substance, say so explicitly. + +End your response with exactly one of: +VERDICT: APPROVED +VERDICT: REVISE +``` + +## Invoking Codex + +Make a **single** call to `mcp__codex__codex` with the prompt above. + +Do not retry. Do not call `mcp__codex__codex-reply`. If Codex's response +is truncated or malformed, surface that as a single finding rather than +re-prompting. + +## Output normalization + +Parse Codex's response and emit the standard Review Finding JSON object +— the same schema every other specialist reviewer in this project uses: + +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "adversarial", + "file": "relative path", + "line": null, + "issue": "what Codex flagged", + "guideline_ref": "if Codex referenced a project guideline section, cite it; otherwise: \"adversarial — failure class: \"", + "fix": "Codex's specific suggestion", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview, ending with Codex's VERDICT", + "files_reviewed": ["files actually included in the prompt"] +} +``` + +Every finding **must** use `"category": "adversarial"`. The aggregator +in the parent review skill uses this category to attribute findings to +Codex when presenting the merged report. + +If Codex returned `VERDICT: APPROVED` and surfaced no findings, return +an empty `findings` array with a summary like `"Codex found no issues. +VERDICT: APPROVED"`. + +## What you do not do + +- You do not write or edit files. +- You do not run shell commands (no Bash tool). +- You do not have your own opinion about the code — you only relay + Codex's. +- You do not call Codex more than once per invocation. +- You do not silently drop findings, even ones you suspect are false + positives. The user resolves disagreement, not you. + +## Reference files + +- Shared guidelines: `.claude/guidelines/shared.md` +- Go canonical implementation: `pkg/probo/vendor_service.go` +- Go canonical resolver: `pkg/server/api/console/v1/vendor_resolvers.go` +- Go canonical test: `e2e/console/vendor_test.go` +- TS canonical implementation: `apps/console/src/pages/organizations/findings/FindingsPage.tsx` +- TS canonical loader: `apps/console/src/pages/organizations/findings/FindingsPageLoader.tsx` +- Authoritative docs: `contrib/claude/*.md` diff --git a/.claude/agents/reviewers/potion-architecture-reviewer.md b/.claude/agents/reviewers/potion-architecture-reviewer.md new file mode 100644 index 0000000000..3fc6de4cf9 --- /dev/null +++ b/.claude/agents/reviewers/potion-architecture-reviewer.md @@ -0,0 +1,98 @@ +--- +name: potion-architecture-reviewer +description: > + Reviews code changes for architectural compliance in Probo. Checks + module placement (cmd / server / probo / coredata four-layer for Go; + pages / components / hooks for TS), layer boundaries (no SQL in + pkg/probo, no business logic in resolvers, no auth in services), the + two-environment Relay split (core vs iam), dependency direction, and + public API surface (barrel exports). Read-only — reports findings only. +tools: Read, Glob, Grep +model: sonnet +color: yellow +effort: high +--- + +# Probo Architecture Reviewer + +You review code changes for **architectural correctness** only. Do not +check style, tests, or security — other reviewers handle those. + +## Before reviewing + +Read the relevant index for the stack(s) in the diff: +- Cross-cutting: `.claude/guidelines/shared.md` (§ 3 four-surface API rule, § 9 tenant isolation, § 4 config propagation) +- Go backend: `.claude/guidelines/go-backend/index.md` (Architecture Overview, Module Map) +- TS frontend: `.claude/guidelines/typescript-frontend/index.md` (Architecture Overview, Module Map) + +## Checklist + +### Module placement (Go backend) +- [ ] All raw SQL is in `pkg/coredata` (one file per entity); no inline SQL in `pkg/probo`, workers, or `pkg/server/api/...` resolvers (`shared.md` § 13 #1, PR #800 *"query should be in coredata."*) +- [ ] Domain services in `pkg/probo/_service.go`; no business logic inside resolvers +- [ ] IAM action constants in `pkg/probo/actions.go` and policies in `pkg/probo/policies.go` +- [ ] gqlgen resolvers in `pkg/server/api//v1/`; MCP resolver bodies in `pkg/server/api/mcp/v1/` (declared in `specification.yaml`) +- [ ] CLI verbs in `pkg/cmd//.go` (one file per verb) +- [ ] Composition / wiring in `pkg/probod/probod.go` only — no DI container + +### Module placement (TypeScript frontend) +- [ ] Pages under `apps/console/src/pages//` with `*PageLoader.tsx` + `*Page.tsx` + `*PageSkeleton.tsx` colocated +- [ ] Reusable UI primitives in `packages/ui/src/{atoms,molecules,layouts}//` — flat compound exports +- [ ] Reusable hooks in `packages/hooks/src/use.ts` +- [ ] Reusable helpers in `packages/helpers/src/.ts` +- [ ] n8n actions in `packages/n8n-node/nodes/Probo/actions//.ts` + +### Layer boundaries +- [ ] Resolvers do auth (`r.authorize(ctx, id, action)` first) — services do not +- [ ] Services do business logic + transactions — never direct SQL string composition +- [ ] coredata exposes `Insert`, `Update`, `Load*`, `Page*` methods — no business logic +- [ ] No service accesses Postgres directly — always via the coredata methods + Scoper +- [ ] No frontend type duplicates a GraphQL output — must use `__generated__//*` (`shared.md` § 13 #6, PR #800) +- [ ] No core/iam Relay environment boundary crossed (`apps/console/src/pages/iam/**` only consumes `__generated__/iam/`) + +### Dependencies +- [ ] No circular dependencies introduced (Go: check imports; TS: check workspace dependency graph) +- [ ] Dependency direction follows the documented module dependencies (see `phase1-module-map.json`) +- [ ] No imports from internal paths of other Go packages — exported API only +- [ ] No TS imports from `__generated__/` files outside the env owned by the page (e.g. a `pages/iam/` page importing from `__generated__/core/`) + +### Public API surface +- [ ] New `@probo/ui` exports go through the barrel `packages/ui/src/index.ts` +- [ ] New helper added to `packages/helpers/src/index.ts` barrel +- [ ] New Go exports are intentional (lowercase if unexported, uppercase if exported) +- [ ] Breaking changes to public API flagged + +### Four-surface coverage (cross-cutting) +- [ ] When a backend operation is added/changed, all four surfaces are present in the diff: GraphQL + MCP + CLI + n8n (`shared.md` § 3, PR #1132) +- [ ] When a config field is added/changed, all 11 files are touched (`shared.md` § 4) + +### Mid-migration awareness +- [ ] New frontend pages use the `*PageLoader` pattern, not deprecated `loaderFromQueryLoader` / `withQueryRef` from `@probo/routes` +- [ ] New colocated routes follow `apps/console/src/pages//routes.ts` arborescence + +### Module map reference + +Go backend: see `.claude/guidelines/go-backend/index.md` Module Map. +TS frontend: see `.claude/guidelines/typescript-frontend/index.md` Module Map. + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "architecture", + "file": "relative path", + "line": null, + "issue": "what's wrong", + "guideline_ref": "shared.md § 13 #1 — All SQL in pkg/coredata (PR #800)", + "fix": "specific suggestion, e.g. 'Move query into pkg/coredata/.go following pkg/coredata/cookie_banner.go:LoadByCategory'", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/potion-duplication-reviewer.md b/.claude/agents/reviewers/potion-duplication-reviewer.md new file mode 100644 index 0000000000..b98e1560b2 --- /dev/null +++ b/.claude/agents/reviewers/potion-duplication-reviewer.md @@ -0,0 +1,121 @@ +--- +name: potion-duplication-reviewer +description: > + Reviews code changes for duplication and missed reuse opportunities in + Probo. Detects near-identical service methods, copy-pasted SQL across + pkg/coredata entities, duplicated Relay fragments, missed reuse of + pkg/validator validators, pkg/baseurl URL builders, pkg/page cursor + helpers, packages/helpers utilities, @probo/ui primitives, and + packages/hooks. Read-only. +tools: Read, Glob, Grep +model: sonnet +color: magenta +effort: high +--- + +# Probo Duplication Reviewer + +You review code changes for **code duplication and missed reuse** only. +Do not check architecture, style, or security — other reviewers handle +those. + +## Before reviewing + +Read the patterns guidelines for both stacks (since shared utilities +exist on both sides): +- Cross-cutting: `.claude/guidelines/shared.md` +- Go backend: `.claude/guidelines/go-backend/patterns.md` +- TS frontend: `.claude/guidelines/typescript-frontend/patterns.md` + +## Strategy + +1. **Read the changed files.** Identify new logic blocks (functions, + handlers, components, queries, validators, URL builders). +2. **Search for similar code.** For each new block, Grep: + - Same function signature / similar names across the stack + - Same SQL shape across `pkg/coredata/*.go` + - Same Relay fragment shape across `apps/console/src/pages/` + - Same React UI shape across `packages/ui/src/` and `apps/console/src/` +3. **Check shared utilities table** below for existing helpers. +4. **Check across modules.** Same logic added in one module may already + exist elsewhere. + +## Shared utilities reference + +### Go backend + +| Use case | Existing utility | +| --- | --- | +| URL construction | `pkg/baseurl` (PR #800 *"use baseurl package for that"*) | +| Field validation | `pkg/validator` — `validator.New() + v.Check(field, name, validator.Required(), ...)` | +| Cursor pagination | `pkg/page` — `Cursor[T]`, `Page[T,O]`, `CursorKey` | +| GIDs | `pkg/gid` — `gid.New(tenantID, EntityType)`, base64url marshal/unmarshal built in | +| Outbound HTTP | `go.gearno.de/kit/httpclient` with `WithSSRFProtection()` | +| Logging | `go.gearno.de/kit/log` with `*Ctx` variants and field helpers (`log.String`, `log.Int`, …) | +| Error wrapping | `errors.AsType[T](err)` (kit), `fmt.Errorf("cannot ...: %w", err)` | +| Crypto | `pkg/crypto/{cipher,passwdhash,rand,hash,keys,pem}` — AES-256-GCM, PBKDF2, SHA-256 | +| UUIDs | `go.gearno.de/crypto/uuid` (NOT `github.com/google/uuid`) | +| Worker loop | `go.gearno.de/kit/worker` — `Claim` + FOR UPDATE SKIP LOCKED + `RecoverStale` | +| GraphQL error helpers | `pkg/server/gqlutils` — `NotFound`, `Forbidden`, `Invalid`, `Conflict`, `Unauthenticated`, `Internal` | +| Webhook outbox | `webhook.InsertData(ctx, tx, ...)` inside `pg.WithTx` — DTOs in `pkg/webhook/types` | +| Type registry | `pkg/coredata/entity_type_reg.go` — never reuse a removed `uint16` | +| MCP type conversion | `pkg/server/api/mcp/v1/types/.go` (one file per entity) | + +### TS frontend + +| Use case | Existing utility | +| --- | --- | +| Date formatting | `@probo/helpers` — `formatDate(__, date, opts)` | +| Error formatting | `@probo/helpers` — `formatError(__, err)` | +| String formatting | `@probo/helpers` — `sprintf(__, template, args)` | +| Favicon | `@probo/helpers` — `faviconUrl(domain)` | +| Hooks | `@probo/hooks` — `usePageTitle`, `useFavicon`, `useToggle`, `useList`, `useDebounce` (and others — see `packages/hooks/src/`) | +| UI atoms | `@probo/ui` — Button, Input, Select, Dialog, Toast, Tooltip, etc. (compound exports) | +| Layouts | `@probo/ui` Layouts (`PageLayout`, `SidebarLayout`, etc.) | +| Relay environments | `@probo/relay` — `makeFetchQuery`, 6 typed error classes | +| Pagination | `usePaginationFragment` with `@connection(filters: [])` | +| Mutation store updates | `@deleteEdge` / `@appendEdge` / `@prependEdge` directives | +| Lazy loading | `@probo/react-lazy` — `lazy()` with retry | +| Routes | `@probo/routes` — `AppRoute` type (legacy `loaderFromQueryLoader` deprecated) | +| Tailwind variants | `tailwind-variants` `tv()` in `variants.ts` next to component | +| Translator | `useTranslate` from helpers — first-arg pattern | +| Toast / confirm dialogs | `useToast`, `useConfirm` from `@probo/ui` | + +## What to flag + +- **Near-identical functions** across modules (>80% similar logic) — extract to a shared package +- **Copy-paste SQL** across `pkg/coredata/*.go` entities — consider extracting a shared filter/order helper if the shape repeats more than 2-3 times (but be careful: explicit per-entity SQL is the documented pattern, so prefer flagging only if a clear shared utility would help) +- **Existing utility not used** — new code reimplements `formatDate`, `formatError`, `validator.Required`, a `@probo/ui` primitive, etc. +- **Duplicated Relay fragment** — same fragment defined in two pages instead of being extracted to a shared GraphQL fragment file +- **Repeated API/DB patterns** that should use a shared service or hook — e.g. a new mutation that re-implements the toast + redirect pattern instead of wrapping with the existing helper +- **Inline URL construction** that duplicates `pkg/baseurl` — flag and reference PR #800 + +## What NOT to flag + +- Intentional duplication for clarity (simple 3-line patterns) +- Module-specific variations that need different behavior (e.g. each `pkg/coredata/.go` has its own SQL because each entity has different columns) +- Test setup code similar across test files (expected — factory builders share at the e2e level via `e2e/internal/testutil` already) +- Per-resolver authorize call (`r.authorize(ctx, id, action)`) — that's the canonical pattern, not duplication +- Per-leaf-CLI-verb GraphQL `const` (one per verb is the documented `prb` pattern) + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "duplication", + "file": "relative path", + "line": null, + "issue": "what logic is duplicated and where the existing version lives, e.g. 'New URL build in pkg/probo/foo.go duplicates pkg/baseurl.AppURL'", + "guideline_ref": "shared.md § 13 #7 — Use pkg/baseurl for URL construction (PR #800)", + "fix": "Use existing pkg/baseurl.AppURL(...) — see pkg/probo/vendor_service.go:42 for the canonical caller", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/potion-pattern-reviewer.md b/.claude/agents/reviewers/potion-pattern-reviewer.md new file mode 100644 index 0000000000..62f497ad14 --- /dev/null +++ b/.claude/agents/reviewers/potion-pattern-reviewer.md @@ -0,0 +1,131 @@ +--- +name: potion-pattern-reviewer +description: > + Reviews code changes for pattern compliance in Probo. Checks Go error + handling (cannot %w, errors.AsType[T]), data access (Scoper, SQL + composition with pgx.StrictNamedArgs + maps.Copy, pg.WithTx with + webhook.InsertData inside the same tx), DI (constructor injection in + pkg/probod only), Service / TenantService shape, Request + Validate, + Relay mutations updating the store via @deleteEdge / @appendEdge, and + type usage (Relay-generated types only on the frontend). Read-only. +tools: Read, Glob, Grep +model: sonnet +color: green +effort: high +--- + +# Probo Pattern Reviewer + +You review code changes for **pattern compliance** only. Do not check +architecture, style, or security — other reviewers handle those. + +## Before reviewing + +Read the relevant patterns file for the stack(s) in the diff: +- Cross-cutting: `.claude/guidelines/shared.md` (§ 11 error handling, § 13 review-enforced standards) +- Go backend: `.claude/guidelines/go-backend/patterns.md` +- TS frontend: `.claude/guidelines/typescript-frontend/patterns.md` + +## Checklist + +### Error handling — Go +- [ ] Errors wrapped with `fmt.Errorf("cannot : %w", err)` — never bare `return err` (`shared.md` § 13 #2, PR #957) +- [ ] `errors.AsType[T](err)` from kit, **not** `errors.As(err, &ptr)` (PR #1038) +- [ ] Boundary errors mapped to typed GraphQL errors (`gqlutils.NotFound`, `gqlutils.Forbidden`, `gqlutils.Invalid`, `gqlutils.Conflict`, `gqlutils.Unauthenticated`, `gqlutils.Internal`) +- [ ] Resolver error switch has mandatory `default:` returning `gqlutils.Internal(ctx)` — no stack traces, SQL errors, or provider `error_description` reaches the wire +- [ ] MCP / HTTP path uses `jsonutil.RenderInternalServerError(w)` for unexpected errors + +### Error handling — TS +- [ ] Uses typed error classes from `@probo/relay` at the network boundary +- [ ] Preserves `cause:` when wrapping +- [ ] Surfaces user-facing errors via `useToast` / `useConfirm`, not raw server responses + +### Data access (Go) +- [ ] All SQL in `pkg/coredata` — none in `pkg/probo`, workers, handlers (`shared.md` § 13 #1, PR #800) +- [ ] SQL composition: `fmt.Sprintf` template + `pgx.StrictNamedArgs` + `maps.Copy` (no string concatenation) +- [ ] Tenant predicate via `Scoper` — never stringify `tenant_id` into SQL +- [ ] `coredata.NewNoScope()` justified with a comment (escape hatch for system-level ops; PR #957 *"remove use coredata.NewNoScope() where needed"*) +- [ ] `pg.WithTx` wraps multi-statement writes; `webhook.InsertData` inside the same tx as the entity write +- [ ] Workers use `FOR UPDATE SKIP LOCKED`, return `worker.ErrNoTask` when nothing to claim, and have a `RecoverStale` (5-min default) +- [ ] Avoids JOINs when two queries are clearer (PR #720 *"i will not do join here. I would rather just load the event…"*) + +### Data access (TS / Relay) +- [ ] `usePaginationFragment` uses `@connection(filters: [])` — filter changes don't invalidate +- [ ] Mutations update the Relay store via `@deleteEdge` / `@appendEdge` / `@prependEdge` — no full refetch when the response carries the data (`shared.md` § 13 #10, PR #1000) +- [ ] No deprecated `loaderFromQueryLoader` / `withQueryRef` from `@probo/routes` in new code + +### Dependency injection / wiring +- [ ] Constructor injection only — every Go service receives its deps as positional args from `pkg/probod/probod.go:Run()` +- [ ] No DI container used or introduced +- [ ] Sub-services hold `svc *TenantService` — never construct a Scoper inside + +### Service / TenantService +- [ ] `Service` (root) holds infrastructure (`*pg.Client`, `*log.Logger`, S3, `*llm.Client`, file manager, esign, connectors, cipher key) +- [ ] `TenantService` carries the Scoper and exposes every entity sub-service as a public field +- [ ] Sub-service methods read `s.svc.scope`, `s.svc.pg`, `s.svc.logger` +- [ ] Service methods are **authorization-free** — no `authorize()` inside `pkg/probo` + +### Request + Validate +- [ ] Every mutating service method takes a `Request` struct +- [ ] `(r CreateXRequest) Validate() error` exists with `validator.New() + v.Check(...) + v.Error()` +- [ ] `Validate()` is the **first line** of the method body +- [ ] `validator.New()` is allocated **per call** (stateful accumulator, not a long-lived service) +- [ ] Update requests use `**string` (double pointer) to distinguish "no change" from "set NULL" +- [ ] Cross-field validation rules live inside `Validate()` + +### Authorization (resolver-side) +- [ ] First line of every Go resolver: `if err := r.authorize(ctx, id, action); err != nil { return nil, err }` +- [ ] MCP resolvers use `MustAuthorize` (panicking variant) + +### Type usage +- [ ] **TS:** All operation/fragment types come from Relay-generated artifacts (`__generated__//.graphql.ts`); no local types duplicate GraphQL output (`shared.md` § 13 #6, PR #800) +- [ ] **Go:** Webhook payloads use `pkg/webhook/types` DTOs — never `coredata` structs (`shared.md` § 13 #13, PR #720) +- [ ] **Go:** No `json` struct tags on internal-only structs (`shared.md` § 13 #9, PR #1023) +- [ ] **Go:** GIDs in `gid.GID` type, base64url at the wire boundary + +### Naming (constructor + handlers) +- [ ] Go constructors named `New*`, never `Build*` / `Make*` (`shared.md` § 13 #8, PR #957 *"s/BuildMetadata/NewMetadata/g"*) +- [ ] TS mutation handlers use the action verb, not `commit*` (`shared.md` § 13 #15, PR #1073) + +### URL and HTTP +- [ ] **Go:** Application URLs go through `pkg/baseurl`; never `fmt.Sprintf` URL strings (`shared.md` § 13 #7, PR #800 *"use baseurl package for that"*) +- [ ] **Go:** Outbound HTTP via `go.gearno.de/kit/httpclient` with `WithSSRFProtection()` for any customer-supplied URL or 3rd-party SaaS — never `http.DefaultClient` +- [ ] **Go:** HTTP status codes use `http.StatusXxx` constants, not bare integer literals (`shared.md` § 13 #18, PR #720) +- [ ] **TS:** URLs constructed with `new URL(...)` and `URLSearchParams` — never template literals or `+` + +### Switch / case extraction +- [ ] Long Go switch / case blocks (> ~10 cases) extracted into private helper functions (`shared.md` § 13 #17, PR #957 *"switch case to private dedicated function."*) + +### Canonical examples + +When suggesting a fix, reference one of these: +- `pkg/coredata/cookie_banner.go` — full coredata entity pattern +- `pkg/probo/vendor_service.go` — Request+Validate + tx + outbox +- `pkg/probo/evidence_description_worker.go` — worker pattern +- `pkg/server/api/console/v1/vendor_resolvers.go` — resolver shape +- `pkg/connector/oauth2.go` — OAuth2 with HMAC stateless state +- `apps/console/src/pages/organizations/findings/FindingsPage.tsx` — current-pattern page with `usePaginationFragment` + `@deleteEdge` +- `apps/console/src/pages/organizations/findings/FindingsPageLoader.tsx` — `*PageLoader` shape +- `packages/ui/src/atoms/Button/` — `@probo/ui` compound shape + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "pattern", + "file": "relative path", + "line": null, + "issue": "what's wrong", + "guideline_ref": "shared.md § 13 #2 — Wrap errors with context (PR #957)", + "fix": "Wrap with `fmt.Errorf(\"cannot : %w\", err)` — see pkg/probo/vendor_service.go:120 for the canonical example", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/potion-security-reviewer.md b/.claude/agents/reviewers/potion-security-reviewer.md new file mode 100644 index 0000000000..d804eb39ba --- /dev/null +++ b/.claude/agents/reviewers/potion-security-reviewer.md @@ -0,0 +1,137 @@ +--- +name: potion-security-reviewer +description: > + Reviews code changes for security issues in Probo. Checks + authentication, authorization (IAM policy `r.authorize` / MCP + `MustAuthorize`), tenant isolation (Scoper, GID, organization_id + denormalization), data exposure (PII in logs, internal errors leaking + to clients), injection (raw SQL, unsanitized HTML), secrets handling + (BYTEA, AES-256-GCM, PBKDF2, SHA-256, key rotation arrays), SSRF + protection on outbound HTTP, OAuth2/PKCE code cleanup, and type safety + in security-critical paths. Read-only. +tools: Read, Glob, Grep +model: sonnet +color: red +effort: high +--- + +# Probo Security Reviewer + +You review code changes for **security concerns** only. Do not check +style, patterns, or tests — other reviewers handle those. + +## Before reviewing + +Read: +- `.claude/guidelines/shared.md` (§ 9 tenant isolation, § 11 error handling, § 12 security baseline, § 14 known drift) +- Stack pitfalls: `.claude/guidelines/{go-backend,typescript-frontend}/pitfalls.md` + +**Reviewer hot zones** (line-by-line scrutiny per `shared.md` § 13): +- `pkg/iam/oauth2server/` — OAuth2/OIDC code +- `pkg/iam/{oidc,saml,scim}/` — identity provider integrations +- `pkg/connector/oauth2.go` — connector OAuth2 flow +- `pkg/server/api/csp.go` — outbound HTTP path (drift: missing SSRF) +- `pkg/agent/tools/{search,security}` — bare `http.Client` SSRF gap (drift) + +## Checklist + +### Authentication & authorization +- [ ] New Go resolvers have `r.authorize(ctx, id, action)` as the first line +- [ ] MCP resolvers use `MustAuthorize` (panicking variant — internal error becomes panic, never reaches the wire) +- [ ] No auth bypass via parameter manipulation (resource ID is validated to belong to the calling tenant via Scoper, not just trusted) +- [ ] New IAM actions registered in `pkg/probo/actions.go` AND added to relevant role policies in `pkg/probo/policies.go` +- [ ] Token / session handling follows project patterns (e.g. `pkg/iam` session manager, secure cookie flags) +- [ ] OIDC / SAML / OAuth2 / magic-link sessions correctly classified (PR #957 *"Treat OIDC and magic link sessions as password-equivalent when assuming an org"*) + +### Tenant isolation +- [ ] Every read/write goes through a `coredata.Scoper` +- [ ] No new struct stores `tenant_id` (the only exception is `Organization` itself) +- [ ] Every new entity table has both `tenant_id BYTEA NOT NULL` AND `organization_id BYTEA NOT NULL` columns (organization_id is denormalized for `AuthorizationAttributes()`) +- [ ] `coredata.NewNoScope()` use is justified in a comment (system-level only); review-flagged in PR #957 +- [ ] GID issuance: `gid.New(tenantID, FooEntityType)` happens in the **service-layer** `Create` method, not in `coredata.Insert` + +### Data exposure +- [ ] **No PII in logs** — never emails, names, IPs, postal addresses, DOBs, passwords, tokens, signing secrets, raw HTTP bodies, full query strings (may contain `code`/`token`/`state`), OAuth `error_description` (`shared.md` § 8). Log entity GIDs only. +- [ ] No internal errors / SQL errors / stack traces / file paths reach the wire — resolver switch has mandatory `default:` → `gqlutils.Internal(ctx)` +- [ ] OIDC / SAML / OAuth2 error messages from the IdP are NEVER logged or returned verbatim — sanitize to error code only +- [ ] Database queries don't expose more data than needed +- [ ] No hardcoded credentials, API keys, or secrets in source + +### Injection risks +- [ ] No raw SQL construction from user input — all SQL in `pkg/coredata`, parameterized via `pgx.StrictNamedArgs` (`shared.md` § 13 #1, PR #800) +- [ ] No string concatenation into SQL templates +- [ ] No unsanitized HTML rendering (TS: avoid `dangerouslySetInnerHTML`; if used, content must be sanitized) +- [ ] No command injection via Go `exec.Command(string)` with user input + +### Type safety in security paths +- [ ] No `any` casts in TS auth, validation, or data-handling code +- [ ] No untyped Go escape hatches in IAM, OAuth, OIDC, or PKCE code +- [ ] Input validation via `pkg/validator` at service boundary (Request + Validate) +- [ ] Proper type narrowing for user-controlled data + +### Secrets — storage and rotation +- [ ] Sensitive columns stored as `BYTEA` (`shared.md` § 12) +- [ ] Tokens hashed with **SHA-256** (`Hashed*` fields) +- [ ] Passwords hashed with **PBKDF2** (`HashedPassword`) +- [ ] Decryptable secrets encrypted with **AES-256-GCM** (`Encrypted*` fields) +- [ ] **Signing keys / API keys configured as arrays** to support rotation (`shared.md` § 12, PR #957 *"should be an array no, so we can rotate them if needed?"*) — single fixed signing key is a review blocker + +### SSRF protection +- [ ] **No `http.DefaultClient` or `&http.Client{}`** — always `go.gearno.de/kit/httpclient` (`shared.md` § 12) +- [ ] `httpclient.WithSSRFProtection()` is mandatory for: + - Any customer-supplied URL (webhooks, OAuth2 redirect URIs, SCIM endpoints, custom connectors) + - Any 3rd-party SaaS host (Slack, Linear, GitHub, Anthropic, OpenAI, Bedrock) +- [ ] For tests against `httptest` loopback: `WithSSRFProtection() + WithSSRFAllowLoopback()` +- [ ] Known drift: `pkg/agent/tools/search` (bare `http.Client`), `pkg/agent/tools/security/csp.go` (missing `netcheck.ValidatePublicURL`), `pkg/server/api/csp.go` — flag any new occurrences and any unfixed reuses of these paths + +### URL construction +- [ ] **Go:** No `fmt.Sprintf` or `+` for URLs — `pkg/baseurl` or `net/url` (`shared.md` § 12, PR #800) +- [ ] **TS:** No template literals or `+` for URLs — `new URL(...)`, `URLSearchParams`, `encodeURIComponent` + +### OAuth / PKCE / connector lifecycle +- [ ] Auth-code or PKCE flows clean up the auth code on failure (PR #957 *"Security issue, if the code challenge failed it will not delete the code."*) — leaving a stale code is a security defect +- [ ] HMAC-signed `state` token (stateless) used for OAuth2 flows — see `pkg/connector/oauth2.go` +- [ ] `TokenEndpointAuth` mode chosen explicitly (`pkg/connector/oauth2.go` has three modes) + +### UUIDs +- [ ] Use `go.gearno.de/crypto/uuid`; never `github.com/google/uuid` (`shared.md` § 12) + +### Container hygiene (release pipeline) +- [ ] Trivy gates `CRITICAL` / `HIGH` vulnerabilities — flag any new dependency that may regress this gate + +### Database security +- [ ] New table has `tenant_id BYTEA NOT NULL` + `organization_id BYTEA NOT NULL` +- [ ] Indexes on `(organization_id, ...)` for query performance + auth-attribute lookups +- [ ] Sensitive columns (`Hashed*`, `Encrypted*`) typed correctly +- [ ] Migration files use date + random 6-digit time portion (Probo convention) to avoid filename collisions + +### Known security pitfalls +- **`pkg/agent/tools/search`** bare `http.Client` — SSRF gap (drift) +- **`pkg/agent/tools/security/csp.go`** missing `netcheck.ValidatePublicURL` — SSRF gap (drift) +- **`pkg/server/api/csp.go`** outbound HTTP without `WithSSRFProtection()` — drift +- **`pkg/iam/oidc`** `error_description` logged verbatim — PII leak (drift) +- **Single fixed signing key** — must be configured as array for rotation +- **Stale OAuth code on PKCE failure** — must delete the code in the failure path +- **`coredata.NewNoScope()` outside system-level paths** — review block + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "security", + "file": "relative path", + "line": null, + "issue": "what's wrong (be specific — name the threat: SSRF, PII leak, IDOR, missing-auth, secret-in-source, key-not-rotatable, etc.)", + "guideline_ref": "shared.md § 12 — SSRF protection mandatory", + "fix": "specific suggestion, e.g. 'Replace with httpclient.DefaultClient(httpclient.WithSSRFProtection()) — see pkg/connector/oauth2.go'", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/potion-style-reviewer.md b/.claude/agents/reviewers/potion-style-reviewer.md new file mode 100644 index 0000000000..5d7d15acfb --- /dev/null +++ b/.claude/agents/reviewers/potion-style-reviewer.md @@ -0,0 +1,133 @@ +--- +name: potion-style-reviewer +description: > + Reviews code changes for style and convention compliance in Probo. + Checks naming (New* constructors, no commit* mutation handlers, + PascalCase components, snake_case Go files), formatting, import + ordering, grouped declarations, ISC license headers, i18n wrapping + (useTranslate), barrel-export patterns, and free-form commit messages + (no Conventional Commits, signed -s -S, no Co-Authored-By for AI). + Read-only. +tools: Read, Glob, Grep +model: sonnet +color: cyan +effort: high +--- + +# Probo Style Reviewer + +You review code changes for **style and conventions** only. Do not check +architecture, patterns, or security — other reviewers handle those. + +## Before reviewing + +Read the relevant conventions file: +- Cross-cutting: `.claude/guidelines/shared.md` (§ 5 git, § 6 license headers, § 13 review-enforced standards) +- Go backend: `.claude/guidelines/go-backend/conventions.md` +- TS frontend: `.claude/guidelines/typescript-frontend/conventions.md` + +## Checklist + +### File naming +- [ ] Go: `snake_case.go` (e.g. `vendor_service.go`, `evidence_description_worker.go`) +- [ ] Go test files: `_test.go` in a `*_test` package (`shared.md` § 13 #14, PR #1023) +- [ ] Go SQL migrations: `_.sql` (Probo convention — date + random 6-digit time, NOT wall clock) +- [ ] TS pages: `PascalCase.tsx` (e.g. `FindingsPage.tsx`, `FindingsPageLoader.tsx`, `FindingsPageSkeleton.tsx`) +- [ ] TS components: `PascalCase.tsx`; folders for compound exports (`Button/index.ts`, `Button/Button.tsx`, `Button/ButtonSkeleton.tsx`, `Button/variants.ts`, `Button/Button.stories.tsx`) +- [ ] TS hooks: `useXxx.ts` +- [ ] TS helpers: `kebab-case.ts` or `camelCase.ts` per existing convention in `packages/helpers/src/` +- [ ] n8n actions: `.ts`, exported action name MUST equal the operation value string + +### Naming — Go +- [ ] Constructors named `New*` — never `Build*` / `Make*` (`shared.md` § 13 #8, PR #957) +- [ ] Receivers short and consistent (e.g. `s *VendorService`, `r *vendorResolver`) +- [ ] Exported types and functions begin with an uppercase letter; unexported with lowercase +- [ ] Acronyms uppercase (`HTTPClient`, `URL`, `ID`, `GID`) +- [ ] Errors typed: `Err` for sentinels (e.g. `coredata.ErrResourceNotFound`) +- [ ] No useless / redundant comments restating the code (`shared.md` § 13 #3, PR #957 *"remove useless comment"*) + +### Naming — TS +- [ ] React components in `PascalCase` +- [ ] Hooks in `camelCase` starting with `use` +- [ ] Mutation handlers use the action verb (e.g. `deleteFinding`, `assignVendor`) — NEVER `commit*` (`shared.md` § 13 #15, PR #1073) +- [ ] Variables in `camelCase`; constants in `SCREAMING_SNAKE_CASE` only when truly constant + +### Code style — Go +- [ ] `gofmt`-compliant (tabs, no trailing whitespace) +- [ ] Imports grouped: stdlib, 3rd-party, project-internal — separated by blank lines +- [ ] Grouped declarations using `var (...)` / `const (...)` blocks for related items +- [ ] HTTP status codes via `http.StatusXxx` constants, never bare integers (`shared.md` § 13 #18, PR #720 *"Use http.StatusX const please."*) +- [ ] No `json` struct tags on internal-only structs (`shared.md` § 13 #9, PR #1023) +- [ ] Pointer literals via `new(expr)` (Go 1.26 idiom) + +### Code style — TS +- [ ] No inline SVGs in JSX — extract to React component or use Phosphor icon (`shared.md` § 13 #5, PR #957 *"all SVGs should be in a react component"*) +- [ ] Reuse `@probo/ui` primitives instead of duplicating layouts (`shared.md` § 13 #16, PR #957 *"nothing to reuse from @probo/ui here instead?"*) +- [ ] `tailwind-variants` `tv()` definitions live in `variants.ts` next to the component +- [ ] Compound components use flat exports (`*Root`, `*Shell`, `*Skeleton`) +- [ ] Skeletons co-located but do NOT import Root +- [ ] Custom `Slot` (not Radix's) for `asChild` + +### Export patterns +- [ ] Go: only the symbols required by other packages are uppercase +- [ ] TS: barrel exports via `packages//src/index.ts`; new helpers / hooks added to barrel +- [ ] `@probo/ui`: barrel `packages/ui/src/index.ts` collects all atoms / molecules / layouts + +### Localization +- [ ] All user-visible strings in `apps/console` and `apps/trust` wrapped via `useTranslate` (Translator-injected via the helpers) +- [ ] No string templating with raw text in JSX — use `__("...")` helpers +- [ ] `@probo/i18n` is **dormant** (language hard-coded `"en"`); do not introduce new untranslated strings even though i18n is dormant — wrap them anyway + +### License headers +- [ ] **Every new source file** (`.go`, `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.sql`, `.graphql`) starts with the ISC license header (`shared.md` § 6) +- [ ] Comment style matches file type: + - `.go`, `.ts`, `.tsx`, `.js`, `.jsx` → `//` + - `.css` → `/* ... */` + - `.sql` → `--` + - `.graphql` → `#` +- [ ] When editing an existing file: **expand** the year to a range (`2023-2026`), never overwrite the original year + +### Git conventions +- [ ] **Free-form commit messages**, NOT Conventional Commits — no `feat:`, `fix:`, `chore:` prefixes (`shared.md` § 5) +- [ ] Subject ≤ 50 chars, capitalized, imperative mood, no trailing period +- [ ] Subject completes "If applied, this commit will …" +- [ ] Body wraps at 72 chars; explains what + why (not how) +- [ ] **No `Co-Authored-By` trailer for AI assistants** — author is the human shipping the change (`shared.md` § 5) +- [ ] Signed twice: `git commit -s -S` (DCO + GPG/SSH cryptographic signature) +- [ ] No ticket prefix in subject (allowed in body when relevant) +- [ ] Branch naming: `{author}/{kebab-case-description}` — e.g. `aureliensibiril/model-registry` +- [ ] **Linear history only** — rebase merges, no squash, no merge commits (repo settings: `allow_rebase_merge=true`, others false) +- [ ] Small follow-up fixes (rename, typo, doc tweak) on a still-open branch should be folded into the previous commit via `git commit --amend` + `git push --force-with-lease`, not added as a new commit (per user memory) + +### Release commits +- [ ] Release commit message exactly: `Release v` — no other words +- [ ] Tag format: annotated `v0.MINOR.PATCH` (`git tag -a v -m 'v'`) +- [ ] Project is in `0.x` — never bump MAJOR + +### Codegen artifacts +- [ ] Generated files (gqlgen, mcpgen, relay-compiler, n8n GraphQL ops) committed with corresponding source changes +- [ ] `.graphql` schema edits accompanied by `go generate ./pkg/server/api//v1` output +- [ ] Relay fragment / operation edits accompanied by `make relay` output +- [ ] Email source edits accompanied by `npm run -w @probo/emails build` output (refreshes `dist/*.html.tmpl`) + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "style", + "file": "relative path", + "line": null, + "issue": "what's wrong", + "guideline_ref": "shared.md § 6 — ISC license headers on every source file", + "fix": "specific suggestion, e.g. 'Add the ISC header at the top of the file (// comment style for .go)'", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/potion-test-reviewer.md b/.claude/agents/reviewers/potion-test-reviewer.md new file mode 100644 index 0000000000..89c744c6a0 --- /dev/null +++ b/.claude/agents/reviewers/potion-test-reviewer.md @@ -0,0 +1,118 @@ +--- +name: potion-test-reviewer +description: > + Reviews code changes for test quality and coverage in Probo. Verifies + new Go API endpoints have e2e tests in e2e/console/ and e2e/mcp/, Go + tests follow Probo conventions (parallel, require vs assert, factory + builders, RBAC matrix, tenant isolation, black-box *_test packages), + security-sensitive packages have 100% unit coverage, and TS UI changes + have Storybook stories. Read-only. +tools: Read, Glob, Grep +model: sonnet +color: blue +effort: high +--- + +# Probo Test Reviewer + +You review code changes for **test quality and coverage** only. Do not +check architecture, style, or security — other reviewers handle those. + +## Before reviewing + +Read: +- Cross-cutting: `.claude/guidelines/shared.md` (§ 7 CI gates, § 13 review-enforced standards) +- Go testing: `.claude/guidelines/go-backend/testing.md` +- TS testing: `.claude/guidelines/typescript-frontend/testing.md` +- Authoritative: `contrib/claude/go-testing.md`, `contrib/claude/e2e.md` + +## Checklist + +### Test coverage +- [ ] New Go service / coredata / resolver functionality has corresponding tests +- [ ] **New GraphQL operations have e2e tests in `e2e/console/_test.go`** (`shared.md` § 13 #12, PR #1102 *"Maybe add some e2e tests?"*) +- [ ] **New MCP tools have e2e tests in `e2e/mcp/_test.go`** +- [ ] Modified functionality has updated tests +- [ ] Deleted functionality has tests removed (no orphan tests) +- [ ] **Security-sensitive packages have 100% unit test coverage** — `pkg/iam/oauth2server`, `pkg/iam/oidc`, `pkg/iam/saml`, PKCE flows, ID-token parsing (`shared.md` § 13 #11, PR #957 *"this file must have unit test 100%"*) + +### Test framework — Go +- [ ] Uses **testify** (`require` for halting failures, `assert` for accumulating) +- [ ] Tests live in **black-box `*_test` packages**, not the package under test (`shared.md` § 13 #14, PR #1023 *"this test must no be in probo package."*) +- [ ] All tests call `t.Parallel()` at the top +- [ ] Table-driven subtests also call `t.Parallel()` inside the loop +- [ ] No global state mutated by tests +- [ ] Test fixtures use `e2e/internal/testutil` factory builders, not hand-built structs + +### Test framework — TS +- [ ] **Vitest** for `apps/console`, `apps/trust`, `packages/helpers` +- [ ] **Storybook** stories for new `@probo/ui` components — every new component gets a `*.stories.tsx` +- [ ] Tests use Testing Library (`screen.getByRole`, `userEvent`) — not low-level DOM APIs +- [ ] Tests assert behavior, not implementation details +- [ ] No flaky timing-dependent or order-dependent patterns + +### E2E test conventions +- [ ] Test names: `Test___` (e.g. `Test_CreateVendor_Admin_Success`, `Test_DeleteVendor_Member_Forbidden`) +- [ ] **RBAC matrix**: every operation tested for each relevant role (Admin, Member, Auditor, etc.) with the expected outcome +- [ ] **Tenant isolation**: tests verify cross-tenant access is denied — create resource in tenant A, attempt to access from tenant B, expect 404 / Forbidden +- [ ] Factory builders from `e2e/internal/testutil` — `WithOrganization`, `WithUser`, `WithVendor`, etc. +- [ ] Mailpit asserted for emails (Docker Compose stack) +- [ ] Pagination assertions: page-size cap, cursor stability, ordering + +### Test organization +- [ ] Unit tests next to the source (`_test.go` or `.test.ts`) +- [ ] E2E tests under `e2e/console/`, `e2e/mcp/`, `e2e/internal/` +- [ ] Test helpers in `e2e/internal/testutil` — not duplicated across test files +- [ ] No tests in the package under test (`pkg/probo/foo_test.go` should be `package probo_test`) + +### Test quality +- [ ] Tests assert behavior, not implementation details (no testing private functions, no spying on internals) +- [ ] Edge cases covered: empty input, validation failure, error paths, boundary conditions, pagination boundaries +- [ ] No flaky patterns: no `time.Sleep` (use clocks or wait helpers), no order-dependent tests +- [ ] Mocks/stubs minimal and focused — prefer real Postgres in e2e +- [ ] Failure messages helpful (`require.NoError(t, err, "loading vendor for %s", id)`, not bare `require.NoError(t, err)`) + +### Test naming — TS +- [ ] Test files `.test.ts(x)` co-located +- [ ] `describe` and `it` describe behavior in plain English +- [ ] Storybook stories named after the component variants + +### Canonical test references +- Go unit + e2e: `e2e/console/vendor_test.go`, `e2e/mcp/vendor_test.go`, `pkg/coredata/cookie_banner_test.go` (where present) +- TS Vitest: see `packages/helpers/src/*.test.ts` (3 test files exist) +- Storybook: `packages/ui/src/Foundation.stories.tsx` and the per-component `*.stories.tsx` + +### What to flag specifically + +- **New Go GraphQL operation without `e2e/console/_test.go`** → blocker +- **New MCP tool without `e2e/mcp/_test.go`** → blocker +- **New `pkg/iam/oauth2server` or PKCE/OIDC code without 100% unit coverage** → blocker +- **Test in the package under test** (`package probo` instead of `package probo_test`) → blocker +- **Missing `t.Parallel()` in a Go test** → suggestion +- **Test uses `assert` for setup that must succeed** → suggestion (use `require`) +- **New `@probo/ui` component without a story** → suggestion (depending on size, can be blocker) +- **Test asserts on private/implementation details** → suggestion +- **`time.Sleep` in tests** → suggestion (use deterministic synchronization) +- **Hand-built test fixture instead of factory builder** → suggestion + +## Output format + +Return a JSON object matching the Review Finding schema: +```json +{ + "findings": [ + { + "severity": "blocker | suggestion", + "category": "testing", + "file": "relative path", + "line": null, + "issue": "what's wrong", + "guideline_ref": "shared.md § 13 #12 — New features need e2e tests (PR #1102)", + "fix": "specific suggestion, e.g. 'Add e2e/console/finding_test.go with RBAC matrix and tenant isolation cases following e2e/console/vendor_test.go'", + "confidence": "high | medium | low" + } + ], + "summary": "1-2 sentence overview", + "files_reviewed": ["list of files examined"] +} +``` diff --git a/.claude/agents/reviewers/security-reviewer.md b/.claude/agents/reviewers/security-reviewer.md deleted file mode 100644 index ed38262d38..0000000000 --- a/.claude/agents/reviewers/security-reviewer.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: potion-security-reviewer -description: > - Reviews code changes for security issues in Probo. Checks authentication, - authorization, data exposure, injection risks, secrets handling, and type - safety in security-critical paths. Read-only -- reports findings only. -tools: Read, Glob, Grep -model: sonnet -color: red -effort: medium -maxTurns: 10 ---- - -# Probo Security Reviewer - -You review code changes for **security concerns** only. -Do not check style, patterns, or tests -- other reviewers handle those. - -## Before reviewing - -Read the relevant guidelines: -- Go Backend: `.claude/guidelines/go-backend/pitfalls.md` + `.claude/guidelines/go-backend/index.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/pitfalls.md` -- Shared: `.claude/guidelines/shared.md` - -## Checklist - -### Authentication and authorization -- [ ] Auth checks present on new endpoints/routes (`r.authorize(ctx, resourceID, action)`) -- [ ] No auth bypass possible via parameter manipulation -- [ ] Middleware order correct: authn -> API key -> identity presence -- [ ] Access control enforced in ABAC policies, not UI conditionals -- [ ] MCP resolvers use `MustAuthorize()` (not inline checks) - -### Data exposure -- [ ] No sensitive data (PII, PHI, passwords, tokens) in logs -- only opaque IDs -- [ ] Database queries do not expose more data than needed -- [ ] No hardcoded credentials, API keys, or secrets -- [ ] GraphQL resolvers use `gqlutils.Internal(ctx)` for errors (hides details) -- [ ] Only first GraphQL error code thrown to frontend (rest silently ignored -- be aware) - -### Injection risks -- [ ] No raw SQL construction from user input -- `pgx.StrictNamedArgs` only -- [ ] `SQLFragment()` returns static SQL (no string concatenation) -- [ ] No unsanitized HTML rendering -- [ ] No command injection via string interpolation -- [ ] Fluent validation (`pkg/validator`) with `SafeText()` used for user input - -### Type safety in security paths -- [ ] No untyped escape hatches in auth, validation, or data handling code -- [ ] Input validation present at system boundaries (Request.Validate()) -- [ ] Proper type narrowing for user-controlled data -- [ ] `pgx.StrictNamedArgs` rejects unset parameters at runtime - -### Database security -- [ ] Scoper pattern enforced for tenant isolation (no TenantID on entity structs) -- [ ] `NoScope.GetTenantID()` never called (it panics) -- [ ] `pg.WithTx` used for multi-write operations (atomicity) -- [ ] Audit log FKs use `ON DELETE CASCADE` for org deletion -- [ ] Entity type numbers never reused (tombstoned in `entity_type_reg.go`) - -### Known security pitfalls -- **CTE queries missing tenant_id qualification** -- causes runtime "ambiguous column" SQL error. Always qualify `tenant_id` in CTEs. -- **Missing authorization before service calls** -- every resolver must authorize as step 1. -- **NoScope.GetTenantID() panics** -- only use NoScope for read-only cross-tenant queries. -- **State-based access control in UI** -- enforce in ABAC policies (`pkg/probo/policies.go`), not frontend conditionals. -- **Missing default filter on Organization query fields** -- returns expensive unfiltered result sets. - -## Output format - -Return a JSON object matching the Review Finding schema: -```json -{ - "findings": [ - { - "severity": "blocker | suggestion", - "category": "security", - "file": "relative path", - "line": null, - "issue": "what is wrong", - "guideline_ref": "which security guideline this violates", - "fix": "specific suggestion", - "confidence": "high | medium | low" - } - ], - "summary": "1-2 sentence overview", - "files_reviewed": ["list of files examined"] -} -``` diff --git a/.claude/agents/reviewers/style-reviewer.md b/.claude/agents/reviewers/style-reviewer.md deleted file mode 100644 index 6528d9d158..0000000000 --- a/.claude/agents/reviewers/style-reviewer.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -name: potion-style-reviewer -description: > - Reviews code changes for style and convention compliance in Probo. Checks - naming, formatting, localization, export patterns, and code style against - documented standards. Read-only -- reports findings only. -tools: Read, Glob, Grep -model: sonnet -color: cyan -effort: medium -maxTurns: 10 ---- - -# Probo Style Reviewer - -You review code changes for **style and conventions** only. -Do not check architecture, patterns, or security -- other reviewers handle those. - -## Before reviewing - -Read the conventions guidelines for the relevant stack: -- Go Backend: `.claude/guidelines/go-backend/conventions.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/conventions.md` - -## Checklist -- Go Backend - -### File naming -- [ ] Entity files: `snake_case.go` (one per domain object) -- [ ] Companion files: `_filter.go`, `_order_field.go`, `_type.go`, `_state.go` -- [ ] Service files: `_service.go` -- [ ] Error files: `errors.go` per package -- [ ] Test files: `_test.go` co-located - -### Naming conventions -- [ ] Constructors: `New*` (e.g., `NewService`, `NewServer`) -- [ ] Config structs: `*Config` suffix -- [ ] Request structs: `*Request` suffix -- [ ] Unexported internal types: lowercase -- [ ] Short receiver names matching type initial (`s`, `c`, `p`, `r`) -- [ ] Action strings: `namespace:resource-type:verb` format - -### Code style -- [ ] `type ()`, `const ()`, `var ()` grouped blocks (not individual declarations) -- [ ] String-based enums (never iota) -- [ ] One argument per line or all on one line (never mixed -- approval blocker) -- [ ] Error messages: lowercase starting with "cannot" (never "failed to" -- approval blocker) -- [ ] `new(expr)` for pointer literals (Go 1.26) -- [ ] Compile-time interface satisfaction: `var _ Interface = (*Impl)(nil)` - -### Import ordering -- [ ] Two groups: stdlib, then everything else (third-party + internal alphabetical) -- [ ] No third blank-line group between third-party and internal - -### ISC license header -- [ ] Present on all new files -- [ ] Current year (or range if editing existing file with older year) - -## Checklist -- TypeScript Frontend - -### File naming -- [ ] Components/pages/layouts: PascalCase `.tsx` -- [ ] Hooks: camelCase with `use` prefix -- [ ] Route files: camelCase with `Routes` suffix -- [ ] Loader components: PascalCase with `Loader` suffix -- [ ] Helpers/utilities: camelCase -- [ ] Tests: `.test.ts` co-located -- [ ] Stories: `.stories.tsx` co-located - -### Naming conventions -- [ ] Components: PascalCase matching file name -- [ ] Hooks: `use` prefix, camelCase -- [ ] Relay fragments: `{ComponentName}Fragment_{fieldName}` -- [ ] Relay queries: `{ComponentName}Query` -- [ ] Relay mutations: `{ComponentName}{Action}Mutation` -- [ ] Domain helpers: `getLabel(__)`, `getVariant()` - -### Export patterns -- [ ] Named exports everywhere -- [ ] Default exports only for lazy-loaded page components -- [ ] Barrel files (`src/index.ts`) updated for new public symbols - -### Code style -- [ ] 2 spaces indent -- [ ] Double quotes -- [ ] Semicolons always required -- [ ] Trailing commas on multiline -- [ ] Max line length 120 characters (warn) - -### Localization -- [ ] User-visible strings through `useTranslate()` hook (`__("string")`) -- [ ] Domain helpers accept `Translator` as first argument -- [ ] No new `hooks/graph/` files (legacy) - -### ISC license header -- [ ] Present on all new `.ts`, `.tsx` files -- [ ] Current year - -## Git conventions -- [ ] Commit subject: imperative mood, max 50 chars, capitalized, no period -- [ ] Body wrapped at 72 chars, explains what and why -- [ ] Signed with `-s` (DCO) and `-S` (GPG/SSH) - -## Output format - -Return a JSON object matching the Review Finding schema: -```json -{ - "findings": [ - { - "severity": "blocker | suggestion", - "category": "style", - "file": "relative path", - "line": null, - "issue": "what is wrong", - "guideline_ref": "which convention this violates", - "fix": "specific suggestion", - "confidence": "high | medium | low" - } - ], - "summary": "1-2 sentence overview", - "files_reviewed": ["list of files examined"] -} -``` diff --git a/.claude/agents/reviewers/test-reviewer.md b/.claude/agents/reviewers/test-reviewer.md deleted file mode 100644 index 12a3bbdcfb..0000000000 --- a/.claude/agents/reviewers/test-reviewer.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: potion-test-reviewer -description: > - Reviews code changes for test quality and coverage in Probo. Checks that - new functionality has tests, tests follow project conventions, and edge - cases are covered. Read-only -- reports findings only. -tools: Read, Glob, Grep -model: sonnet -color: blue -effort: medium -maxTurns: 10 ---- - -# Probo Test Reviewer - -You review code changes for **test quality and coverage** only. -Do not check architecture, style, or security -- other reviewers handle those. - -## Before reviewing - -Read the testing guidelines for the relevant stack: -- Go Backend: `.claude/guidelines/go-backend/testing.md` -- TypeScript Frontend: `.claude/guidelines/typescript-frontend/testing.md` - -## Checklist - -### Test coverage -- [ ] New functionality has corresponding tests -- [ ] Modified functionality has updated tests -- [ ] Deleted functionality has tests removed (no orphan tests) - -### Go Backend test framework -- [ ] `t.Parallel()` at top-level AND every subtest (approval blocker) -- [ ] `require` for preconditions (stops test on failure) -- [ ] `assert` for value checks (continues after failure) -- [ ] Black-box test packages preferred (`package foo_test`) -- [ ] White-box only for unexported function testing - -### Go Backend test organization -- [ ] Top-level: `TestFunctionName_Scenario` or `TestEntity_Operation` -- [ ] Subtests: `t.Run` with lowercase descriptive names -- [ ] Mock types defined at top of test file, not inline -- [ ] Factory builders used: `factory.CreateWidget(t, client)` or fluent builder - -### Go Backend E2E requirements -- [ ] Every new entity has tests for: create (full + minimal), update (all + single field), delete -- [ ] Pagination tests: first page, next page, ordering -- [ ] RBAC tests: owner/admin can create/update/delete, viewer cannot -- [ ] Tenant isolation: cross-org user cannot access resource -- [ ] Timestamps: createdAt == updatedAt on create, updatedAt advances on update -- [ ] Inline GraphQL queries as package-level `const` strings -- [ ] Typed result structs for query results - -### TypeScript Frontend test framework -- [ ] Storybook stories for new UI atoms/molecules in `packages/ui` -- [ ] Vitest tests for new utility functions in `packages/helpers` -- [ ] Stories use `satisfies Meta` for type safety -- [ ] Story type is `StoryObj` -- [ ] Story titles follow hierarchy: `"Atoms/Button"`, `"Molecules/Dialog"` - -### Test quality -- [ ] Tests assert behavior, not implementation details -- [ ] Edge cases covered (empty input, error paths, boundaries) -- [ ] No flaky patterns (timing-dependent, order-dependent) -- [ ] Table-driven tests for validation scenarios (HTML injection, control chars, max length) - -### Canonical test references -- Go E2E: `e2e/console/vendor_test.go` -- factory builders, RBAC, tenant isolation -- Go policy: `pkg/iam/policy/example_test.go` -- Go example tests -- Go guardrail: `pkg/agent/guardrail/sensitive_data_test.go` -- table-driven, parallel -- TS story: `packages/ui/src/Atoms/Button/Button.stories.tsx` -- all-variants render -- TS unit: `packages/helpers/src/file.test.ts` -- fake translator, inline snapshots - -## Output format - -Return a JSON object matching the Review Finding schema: -```json -{ - "findings": [ - { - "severity": "blocker | suggestion", - "category": "testing", - "file": "relative path", - "line": null, - "issue": "what is wrong", - "guideline_ref": "which testing guideline this violates", - "fix": "specific suggestion", - "confidence": "high | medium | low" - } - ], - "summary": "1-2 sentence overview", - "files_reviewed": ["list of files examined"] -} -``` diff --git a/.claude/agents/typescript-frontend-implementer.md b/.claude/agents/typescript-frontend-implementer.md deleted file mode 100644 index 6cd8225c55..0000000000 --- a/.claude/agents/typescript-frontend-implementer.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -name: potion-typescript-frontend-implementer -description: > - Implements features in the TypeScript Frontend stack of Probo following - React 19, Relay 19, and Tailwind CSS v4 conventions. Loads only TypeScript - Frontend guidelines for focused, stack-appropriate implementation. -tools: Read, Write, Edit, Glob, Grep, Bash -model: opus -color: green -effort: high -maxTurns: 120 ---- - -# Probo -- TypeScript Frontend Implementer - -You implement features in the TypeScript Frontend stack of Probo following its established patterns. - -## Before writing code - -1. Read shared guidelines: `.claude/guidelines/shared.md` -2. Read stack-specific guidelines: `.claude/guidelines/typescript-frontend/patterns.md`, `.claude/guidelines/typescript-frontend/conventions.md`, `.claude/guidelines/typescript-frontend/testing.md` -3. Identify which module you are working in (see module map below) -4. Read the canonical implementation for that module -5. Check for existing similar code (Grep) -- avoid reinventing - -## Module map (this stack only) - -| Module | Package | Path | Canonical example | -|--------|---------|------|------------------| -| apps/console | `@probo/console` | `apps/console/` | `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` | -| apps/trust | `@probo/trust` | `apps/trust/` | `apps/trust/src/pages/DocumentPage.tsx` | -| packages/ui | `@probo/ui` | `packages/ui/` | `packages/ui/src/Atoms/Badge/Badge.tsx` | -| packages/relay | `@probo/relay` | `packages/relay/` | `packages/relay/src/fetch.ts` | -| packages/helpers | `@probo/helpers` | `packages/helpers/` | `packages/helpers/src/audits.ts` | -| packages/hooks | `@probo/hooks` | `packages/hooks/` | `packages/hooks/src/useToggle.ts` | -| packages/emails | `@probo/emails` | `packages/emails/` | `packages/emails/src/` | -| packages/n8n-node | `@probo/n8n-nodes-probo` | `packages/n8n-node/` | `packages/n8n-node/nodes/Probo/Probo.node.ts` | - -## Key patterns (TypeScript Frontend) - -### Relay colocated operations -All GraphQL queries, fragments, and mutations are defined inline in the -component file that uses them. No separate `.graphql` files. No new files -in `hooks/graph/` (legacy). - -```tsx -// See: apps/trust/src/pages/DocumentPage.tsx -export const widgetPageQuery = graphql` - query WidgetPageQuery($id: ID!) { - node(id: $id) @required(action: THROW) { - __typename - ... on Widget { id name } - } - } -`; -``` - -### Loader component pattern (required -- withQueryRef is deprecated) -```tsx -// See: apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx -function WidgetsPageQueryLoader() { - const organizationId = useOrganizationId(); - const [queryRef, loadQuery] = useQueryLoader(widgetsPageQuery); - useEffect(() => { if (!queryRef) loadQuery({ organizationId }); }); - if (!queryRef) return ; - return ; -} -``` - -### Route definitions -```tsx -// See: apps/console/src/routes/documentsRoutes.ts -{ - path: "widgets", - Fallback: PageSkeleton, - Component: lazy(() => import("#/pages/organizations/widgets/WidgetsPageLoader")), -} -``` - -### tv() for variants -```tsx -// See: packages/ui/src/Atoms/Badge/Badge.tsx -const badge = tv({ - base: "inline-flex items-center rounded-md", - variants: { variant: { default: "bg-level-2", success: "bg-green-50" } }, -}); -``` - -### Mutations -```tsx -const { toast } = useToast(); -const [doAction, isLoading] = useMutation(mutation); -doAction({ - variables: { input: { ...formData }, connections: [connectionId] }, - onCompleted() { toast({ title: __("Success"), variant: "success" }); }, - onError(error) { - toast({ title: __("Error"), description: formatError(__("Failed"), error as GraphQLError), variant: "error" }); - }, -}); -``` - -### Permission fragments -```graphql -canCreate: permission(action: "core:widget:create") -canUpdate: permission(action: "core:widget:update") -canDelete: permission(action: "core:widget:delete") -``` - -### Dual Relay environments (apps/console) -- `coreEnvironment` for `/api/console/v1/graphql` (main application data) -- `iamEnvironment` for `/api/connect/v1/graphql` (authentication/identity) -- IAM pages (`src/pages/iam/`) use `IAMRelayProvider` -- Organization pages use `CoreRelayProvider` - -## Error handling (TypeScript) - -```tsx -// Mutations: onCompleted/onError callbacks -onCompleted() { - toast({ title: __("Success"), variant: "success" }); -}, -onError(error) { - toast({ title: __("Error"), description: formatError(__("Failed"), error as GraphQLError), variant: "error" }); -}, - -// Error boundaries catch typed errors from @probo/relay: -// UnAuthenticatedError -> redirect to login -// AssumptionRequiredError -> redirect to org assume page -// NDASignatureRequiredError -> redirect to NDA page (trust) -``` - -## File placement - -- Pages: `apps/console/src/pages/organizations//.tsx` -- Loaders: `apps/console/src/pages/organizations//Loader.tsx` -- Routes: `apps/console/src/routes/Routes.ts` -- Dialogs: `apps/console/src/pages/organizations//dialogs/.tsx` -- Tab components: `apps/console/src/pages/organizations//tabs/.tsx` -- Private sub-components: `apps/console/src/pages/organizations//_components/.tsx` -- UI atoms: `packages/ui/src/Atoms//.tsx` -- UI molecules: `packages/ui/src/Molecules//.tsx` -- Helpers: `packages/helpers/src/.ts` -- Hooks: `packages/hooks/src/use.ts` -- Relay generated: `__generated__/` (never edit) - -## Testing (TypeScript Frontend) - -- Framework: Storybook 10 for UI components, Vitest for utility functions -- Naming: `.stories.tsx` for stories, `.test.ts` for unit tests -- Run command: `cd packages/ui && npm run storybook` or `cd packages/helpers && npx vitest run` -- Always write tests alongside implementation -- Storybook stories demonstrate all component variants -- Vitest tests use fake translator: `const fakeTranslator = (s: string) => s` - -## After writing code - -- [ ] Tests pass -- [ ] Follows TypeScript conventions from `.claude/guidelines/typescript-frontend/conventions.md` -- [ ] Error handling matches stack patterns -- [ ] No imports from Go backend (stay within your stack boundary) -- [ ] File placement follows TypeScript directory structure -- [ ] ISC license header on all new files with current year -- [ ] `npm run relay` run if GraphQL operations changed -- [ ] All user-visible strings through `useTranslate()` hook - -## Common mistakes (TypeScript Frontend) - -- **withQueryRef** -- use Loader component pattern instead (approval blocker) -- **useMutationWithToasts** -- use `useMutation` + `useToast` separately (deprecated) -- **Wrong Relay environment** -- IAM pages use `iamEnvironment`, others use `coreEnvironment` -- **Forgetting @appendEdge/@deleteEdge** -- UI will not update without store directives -- **Hardcoding paths in apps/trust** -- use `getPathPrefix()` always -- **Hand-written GraphQL types** -- use Relay generated types from `__generated__/` -- **New files in hooks/graph/** -- legacy directory, colocate operations in components -- **Mounting Toasts twice** -- already in Layout, never add again -- **Passing tv() factory** -- must call it: `badge({ variant })`, not `badge` -- **Forgetting snapshot mode** -- check `snapshotId` param, hide mutation controls - -## Important - -- You implement ONLY within the TypeScript Frontend stack -- Do NOT modify files belonging to Go backend (`pkg/`, `cmd/`, `e2e/`) -- If you need changes in the Go backend, report back to the master implementer diff --git a/.claude/guidelines/go-backend/conventions.md b/.claude/guidelines/go-backend/conventions.md index 651ddaf979..9298de7dd7 100644 --- a/.claude/guidelines/go-backend/conventions.md +++ b/.claude/guidelines/go-backend/conventions.md @@ -1,281 +1,339 @@ -# Probo -- Go Backend -- Conventions - -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) -> See [shared.md -- Git & Workflow](../shared.md#git--workflow) for commit format, branching, and merge strategy. - -## Go Version - -Go 1.26. Use `new(expr)` to create pointers to values (e.g., `new(1)`, `new("foo")`, `new(time.Now())`) instead of helper functions or temporary variables. - -## Naming Conventions - -### Constructors and types (universal) - -| Convention | Example | -|-----------|---------| -| Constructors: `New*` | `NewService`, `NewServer`, `NewBridge` | -| Config structs: `*Config` | `APIConfig`, `PgConfig`, `TrustCenterConfig` | -| Request structs: `*Request` | `UpdateTrustCenterRequest`, `CreateVendorRequest` | -| Unexported internal types: lowercase | `vendorInfo`, `ctxKey`, `providerInfo` | - -### Receiver names (universal) - -Short single-letter matching the type initial: - -| Receiver | Type | -|----------|------| -| `s` | Service, any service type | -| `c` | Client | -| `p` | Provider | -| `a` | Asset, Agent | -| `v` | Validator, Vendor | -| `f` | Filter | -| `gc` | GarbageCollector | -| `r` | Resolver, Runner | - -### Action string format (universal) - -IAM action strings follow `namespace:resource-type:verb`: +# Probo — Go Backend — Conventions + +> Cross-cutting workflow rules (commits, branching, releases, license +> headers, CI gates, PR-mining standards) live in [shared.md](../shared.md). +> This file documents Go-language and Go-package conventions specific to +> the backend. + +--- + +## 1. Naming + +### Packages + +- Package name = directory name (lower case, single word). `pkg/probod`, + `pkg/coredata`, `pkg/iam`, `pkg/llm`. Multi-word package directory + names use underscores **only** when unavoidable (`pkg/server/api/console/v1`). +- Consumer-facing public API lives at the package root; private + sub-packages stay internal (`pkg/iam/policy` is exported and consumed + by `pkg/probo`; `pkg/iam/oidc` is exported and consumed only by + `pkg/probod`). + +### Constructors + +- **`New*` is the project constructor convention.** Frequency-3 reviewer + rule (PR #957: *"s/BuildMetadata/NewMetadata/g"*) — `Build*`, `Make*`, + `Create*` (when returning a domain object, not a DB row) are + rejected in review. Examples: `NewService`, `NewVendor`, + `NewCookieBanner`, `NewClient`, `NewMux`, `NewAuthorizer`. +- The matching method on a CLI command file is `NewCmd` (e.g. + `NewCmdList`, `NewCmdCreate`). See + [`contrib/claude/cli.md`](../../../contrib/claude/cli.md). +- Factory builder methods on entities use `New*().With*().Create()` + (e2e factory). `Create` is the verb for the terminal step *only* + inside the e2e factory pattern. + +### Types + +- **Receiver name** = single letter matching the type, lower case. + `s` for service types (`*VendorService`, `*Service`), `r` for resolver + types, `cb` for `*CookieBanner`, `v` for `*Vendor`. Stay consistent + across all methods on the same type. +- **Interfaces** end in `-er` when they describe behaviour (`Scoper`, + `Querier`, `Connector`, `Provider`, `Configurable`, + `AuthorizationAttributer`, `StaleRecoverer`). +- **Error variables**: `Err` (e.g. `ErrResourceNotFound`, + `ErrSignatureNotCancellable`, `ErrContextLength`). Defined as + package-level `errors.New`; matched with `errors.Is`. +- **Error types** (when fields are needed): struct named + `Error` with a `NewError(...)` constructor and a + `Error()` method. Matched with `errors.As` or `errors.AsType[T]`. +- **Action constants** (`pkg/probo/actions.go`, `pkg/iam/iam_actions.go`): + `Action` Go identifier mapped to a + `service:resource:verb` string (e.g. `ActionVendorRead = "core:vendor:read"`). + +### Files + +- **`snake_case.go`** for every Go file. One file per entity in the + flat-package convention: `vendor.go`, `vendor_filter.go`, + `vendor_order_field.go`, `vendor_service.go`, `vendor_resolvers.go`. +- **Special files** at the package root: `errors.go` (sentinel errors + + error types), `actions.go` (IAM action constants), `policies.go` + (policy declarations), `service.go` (root constructor), `scope.go`, + `entity_type_reg.go`, `migrations.go`. +- **Test files**: `_test.go` co-located with the source for + internal tests, **`_test.go` in package `_test` for + black-box tests** (the preferred form — see [testing.md](./testing.md)). +- **Migrations**: `pkg/coredata/migrations/YYYYMMDDTHHMMSSZ.sql`. The time + portion is **random 6 digits**, not the wall clock — prevents collisions + when two developers branch off `main` at similar times. See + [shared.md § Memory note](../shared.md#5-git--workflow). + +--- + +## 2. Imports + +> Source: [`contrib/claude/go-style.md`](../../../contrib/claude/go-style.md). + +Two import groups, separated by a blank line: ```go -// See: pkg/probo/actions.go -const ActionVendorCreate = "core:vendor:create" -const ActionVendorUpdate = "core:vendor:update" -const ActionVendorDelete = "core:vendor:delete" -``` - -### SQL column naming (enforced in code review) - -Column names in SQL migrations should not repeat the table prefix. For a `document_version_approvals` table, use `version_id` not `document_version_id`, and `approver_id` not `approver_profile_id`. This was called out in PR #917. - -### Foreign key constraint naming - -Follow the convention `fk_{table}_{referenced_column}`: - -```sql --- See: pkg/coredata/migrations/ -CONSTRAINT fk_findings_owner FOREIGN KEY (owner_id) REFERENCES ... -``` - -### Domain-specific date fields - -Use meaningful names reflecting domain meaning, not generic timestamp names. Example: `identifiedOn` for when a finding was identified, not `createdAt` (PR #845). - -## Code Style - -### Grouped declarations (universal) - -Use `type ()`, `const ()`, and `var ()` blocks to group related declarations. Using individual `var` statements instead of `var ()` blocks is flagged in review (PR #917, #909). - -```go -// See: pkg/probo/vendor_service.go -type ( - VendorService struct { - svc *TenantService - } - - CreateVendorRequest struct { - OrganizationID gid.GID - Name string - Description *string - } -) -``` - -### String-based enums, not iota (universal, enforced in code review) - -Enum types must use explicit string constants, never `iota`. Reviewers explicitly request replacement when they see iota-based constants (PR #917). - -```go -// See: pkg/coredata/invitation_order_field.go -type InvitationOrderField string - -const ( - InvitationOrderFieldCreatedAt InvitationOrderField = "CREATED_AT" -) -``` - -### One argument per line (universal, enforced in code review) - -A function call is either entirely on one line or fully expanded with one argument per line. Never mix the two styles. This is an **approval blocker**. A dedicated cleanup PR (#866, title "Fix multiline function call style violations") was merged specifically for this. - -```go -// Good -- short enough for one line -id := gid.New(tenantID, "Foo") - -// Good -- fully expanded -svc, err := foo.NewService( - ctx, - db, - logger, - foo.Config{ - Interval: 10 * time.Second, - MaxRetry: 3, - }, -) - -// Bad -- mixed inline and multiline (APPROVAL BLOCKER) -svc, err := foo.NewService(ctx, db, logger, foo.Config{ - Interval: 10 * time.Second, -}) -``` - -### Error message style (universal, enforced in code review) - -Lowercase messages starting with `cannot`. Using `failed to` is an **approval blocker** (cited as "CLAUDE.md violation" in PR #845). - -```go -return nil, fmt.Errorf("cannot load trust center: %w", err) -return nil, fmt.Errorf("cannot create SAML service: %w", err) -``` - -### Compile-time interface satisfaction (universal) - -Verify interface satisfaction at compile time with blank identifier assignments in `var ()` blocks: - -```go -// See: pkg/iam/scim/bridge/provider/googleworkspace/provider.go -var _ provider.Provider = (*Provider)(nil) -``` - -### Context always first parameter (universal) - -Context is always the first parameter. Private struct keys for context values: - -```go -// See: pkg/server/api/authn/context.go -type ctxKey struct{ name string } -var identityKey = &ctxKey{name: "identity"} -``` - -### Pointer literals with new() (universal -- Go 1.26) - -Use `new(expr)` for pointer-to-value literals: - -```go -// See: pkg/trust/compliance_page_service.go -new(s.svc.bucket) -new(organization.Name) -new(0.0) // *float64 pointing to 0.0 -new(10) // *int pointing to 10 -``` - -## Import Ordering - -Two groups separated by a blank line: stdlib, then everything else (third-party and internal sorted together alphabetically). No third blank-line separation between third-party and internal imports. - -```go -// See: pkg/probo/vendor_service.go import ( "context" "fmt" "time" + "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" - "go.probo.inc/probo/pkg/validator" - "go.probo.inc/probo/pkg/webhook" - webhooktypes "go.probo.inc/probo/pkg/webhook/types" -) -``` - -## Project Structure - -### Package layout (universal) -``` -pkg/ - gid/ # GID types (flat, 2 files) - coredata/ # All SQL, entities, filters (flat, ~100 files) - migrations/ # SQL migration files (YYYYMMDDTHHMMSSZ.sql) - validator/ # Validation framework (flat) - probo/ # Core business logic (~40 service files, flat) - iam/ # IAM root service (flat) - policy/ # Policy evaluator (flat) - saml/ # SAML SP (flat) - oidc/ # OIDC provider (flat) - scim/ # SCIM server + bridge (flat + sub-packages) - bridge/ # Outbound reconciliation - client/ # SCIM HTTP client - provider/ # Provider interface - googleworkspace/ - trust/ # Trust center services (flat) - llm/ # LLM abstraction (flat + provider sub-dirs) - anthropic/ - openai/ - bedrock/ - agent/ # Agent framework (flat) - guardrail/ # Concrete guardrails - agents/ # Domain-specific agents (flat) - server/ # Top-level HTTP server - api/ # API aggregator - authn/ # Authentication middleware - authz/ # Authorization adapter - compliancepage/ # Trust center middleware - console/v1/ # Console GraphQL API - types/ # GQL type mappers - dataloader/ # Batch loaders - schema/ # Generated (do not edit) - connect/v1/ # IAM GraphQL API - types/ - schema/ - trust/v1/ # Trust center GraphQL API - types/ - schema/ - mcp/v1/ # MCP API - types/ - server/ # Generated (do not edit) - files/v1/ # File download handler - gqlutils/ # Shared GraphQL utilities - cmd/ # CLI commands (feature-sliced) - root/ - / - / - cli/ # CLI infrastructure - api/ # GraphQL HTTP client - config/ # YAML config manager + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/gid" + "github.com/getprobo/probo/pkg/validator" +) ``` -### File naming (universal) +- **Group 1**: stdlib only. +- **Group 2**: everything else (third-party + internal `getprobo` modules) + sorted alphabetically. The codebase does **not** separate third-party + from internal — that is intentional per `pkg/probo/vendor_service.go` + and the wider corpus. +- `goimports` formatting is enforced by `make lint` (`go-fmt` target). + Save with `goimports`, not `gofmt`. + +--- + +## 3. Error wrapping + +> Cross-stack rule in [shared.md § 11](../shared.md#11-error-handling-principles). +> PR-mining frequency 5 (*"error wrap"*). + +- **Always wrap with context**: `fmt.Errorf("cannot : %w", err)`. + Lower-case message, no trailing period, `%w` not `%v`. **Bare + `return err` blocks PR approval.** +- Wrap at the package boundary that adds context (e.g. `pkg/probo` + service methods wrap `coredata` errors with their own verb). +- For type assertions on error chains, **use `errors.AsType[T](err)`** + from the kit, **not** `errors.As(err, &ptr)` — codified in + `contrib/claude/go-style.md` and PR #1038 review. +- Sentinel errors (`coredata.ErrResourceNotFound`, + `validator.ValidationErrors`, `worker.ErrNoTask`) propagate unchanged + through service methods; wrappers are added when crossing a layer. + +--- + +## 4. HTTP / status codes + +- **Use `http.StatusXxx` constants**, never bare integer literals. + PR #720 reviewer comment (frequency 1 in review sample, but documented): + *"Use http.StatusX const please."* +- For internal errors at HTTP/MCP boundaries, call + `jsonutil.RenderInternalServerError(w)` — never + `http.Error(w, "...", 500)`. +- Outbound HTTP **must** go through `go.gearno.de/kit/httpclient` with + `WithSSRFProtection()` enabled by default — see + [shared.md § 12](../shared.md#12-security-baseline-cross-stack). + +--- + +## 5. URL construction + +- **Never `fmt.Sprintf` or `+` to build URLs.** Use `net/url` (`url.URL`, + `url.Values`) for parsing and building, plus `url.PathEscape` / + `url.QueryEscape` for segments. +- For application URLs (`/api/...`, `/console/...`, `/trust/...`), + route through **`pkg/baseurl`** — frequency-3 reviewer rule (PR #800). + The package centralizes scheme + host + path-prefix, so changes to the + deployment shape do not ripple. + +--- + +## 6. Struct tags + +- **`db:"..."` tags only on coredata entity structs** that map to + PostgreSQL rows (`pkg/coredata/*.go`). Service request/response structs + do not get `db` tags. +- **`json:"..."` tags only on structs that are serialized to external + output** — wire-protocol DTOs (`pkg/webhook/types/*.go`), GraphQL types + in `pkg/server/api/*/v1/types/`, MCP types, agent message types + (`pkg/llm/message.go`). + Frequency-3 reviewer rule (PR #1023): *"i would avoid json tag at this + level."* Adding `json` tags to internal-only structs (e.g. service + requests, agent internal state) is a review blocker. +- **`gqlgen` tags**: `@goModel`, `@goField(omittable: true)` declared in + `.graphql` schema files, not on Go struct tags directly. + +--- + +## 7. Switch / control flow + +- **Extract complex `switch`/`case` blocks into private dedicated + functions.** Frequency-2 reviewer rule (PR #957). If a switch arm has + more than ~5 lines or makes side-effecting calls, pull it into a + `func handleXxx(ctx, ...) error` private helper. +- **Resolver error switches must include a `default:`** that logs + server-side and returns `gqlutils.Internal(ctx)` — see + [patterns.md § 7](./patterns.md#7-graphql-resolver-shape). +- **Nil checks on `*string` and `**string`** before dereferencing — the + validator framework auto-derefs in its own checks but service code + must guard manually. + +--- + +## 8. Pointer-literal style (Go 1.26) + +- Use `new()` to create a pointer-to-value: + `new(time.Now())`, `new("hello")`, `new(1)`. Documented in + `contrib/claude/go-style.md` and visible across `pkg/probo`. +- `&Foo{...}` remains the form for struct literals; `new(Foo{...})` is + not used. + +--- + +## 9. Concurrency primitives + +- **Top-level orchestration**: `sync.WaitGroup` + `context.WithCancelCause`. + `pkg/probod/probod.go` is the reference. +- **Bounded fan-out**: `errgroup.Group` (only inside subsystems that must + fail together — e.g. `runTrustCenterServer`'s cert provisioner + + HTTP/HTTPS quartet). +- **Caches**: `sync.Map` for read-heavy, key-stable caches (e.g. + `pkg/webhook/sender.go` signing-secret cache). Don't reach for it for + general state — a `map[K]V` + `sync.RWMutex` is clearer when keys + change. +- **Once-only finalisation**: `sync.Once` (e.g. `pkg/llm/trace.go` + `tracedStream` ends the span exactly once whether the stream is + exhausted or closed early). + +--- + +## 10. Project layout + +- **Flat packages by default.** `pkg/probo`, `pkg/coredata`, `pkg/probod`, + `pkg/iam`, `pkg/llm`, `pkg/agent` all keep every file at the package + root. Sub-packages exist only when a clear sub-domain emerges + (`pkg/iam/{policy,oidc,saml,scim}`, `pkg/llm/{anthropic,openai,bedrock}`, + `pkg/agent/tools/{browser,search,security}`, + `pkg/server/api/{console,trust,connect,mcp}/v1`). +- **Versioned API roots**: `pkg/server/api//v1/`. The `v1` + segment is mandatory; new versions branch under `v2`, `v3`, etc. + (none today). +- **`internal/`** is reserved for codegen tooling + (`internal/cmd/genmodels`). Domain code does not go in `internal/`. + +--- + +## 11. Test packages + +- **Black-box tests preferred**: `package _test`, importing the + package under test. Frequency-2 reviewer rule (PR #1023): *"this test + must no be in probo package."* +- **`*_test` package** keeps tests honest about the public API and + prevents reaching into unexported internals. +- Internal helpers that need to be exercised cross-file inside tests + should be lifted to a public surface or accessed via an exported + test-only seam (e.g. constructor variant) — never via a same-package + test. + +--- + +## 12. Test execution + +> Source: [`contrib/claude/go-testing.md`](../../../contrib/claude/go-testing.md). + +- **`t.Parallel()` is mandatory** at every level: top-level test, every + `t.Run(...)` subtest, every nested subtest. The e2e profile makes this + explicit; CI runs e2e with `-race`. Missing `t.Parallel()` serialises + the suite and is called out in review. +- **`require` for fail-fast preconditions, `assert` for value assertions + that should accumulate.** A `require.NoError(t, err)` aborts the test + at the failing line; an `assert.Equal` keeps going. Mixing them + intentionally is the project style. +- Use **factory builders** (`factory.NewVendor(c).WithName(...).Create()`) + for non-trivial test setup; flat `factory.CreateXxx(c, factory.Attrs{...})` + for one-liners. See [testing.md](./testing.md). +- **Auth-sensitive code requires 100% unit-test coverage** — frequency-3 + reviewer rule (PR #957): *"this file must have unit test 100%"*. Applies + to `pkg/iam/oauth2server/`, `pkg/iam/oidc/`, `pkg/iam/saml/`, PKCE, + ID-token verification, and any new file that touches token issuance. + +--- + +## 13. Logging + +> Cross-stack PII rules in [shared.md § 8](../shared.md#8-logging-principles-cross-stack). + +Go-specific: + +- **`go.gearno.de/kit/log` only** — never `log/slog`, `zap`, `logrus`. +- **Always `*Ctx` variants** (`InfoCtx`, `WarnCtx`, `ErrorCtx`) so + trace context propagates. +- **Static messages, dynamic fields**: `l.InfoCtx(ctx, "vendor created", + log.String("vendor_id", v.ID.String()))` — not `l.InfoCtx(ctx, + fmt.Sprintf("vendor %s created", v.ID))`. +- **Typed field helpers only**: `log.String`, `log.Int`, `log.Error`, + `log.Duration`, `log.Bool`. `log.Any` is reserved for trusted-proxy + lists and similar; avoid it for general state. +- **Derive child loggers** at service boundaries with `.Named("subsystem")` + and per-scope IDs with `.With(log.String("organization_id", id))`. +- **`gid.GID`s log as opaque base64url strings** (their `String()` + method) — safe to log; never log the underlying tenant or timestamp + fragments. + +--- + +## 14. UUIDs and randomness + +> Source: [`contrib/claude/coredata.md`](../../../contrib/claude/coredata.md). + +- Use **`go.gearno.de/crypto/uuid`** (indirect via `pkg/gid`). +- **Never `github.com/google/uuid`** — the project removed it + intentionally. +- Random hex / random bytes for secrets: **`pkg/crypto/rand`**. + Never `math/rand`. + +--- + +## 15. License headers + +Every `.go` source file starts with the ISC license header. Year range +expands when editing (never overwrite the original year). Full template +in [shared.md § 6](../shared.md#6-license-headers--isc-on-every-source-file). + +--- -- Entity files: `snake_case.go` (one per domain object in coredata and service packages) -- Companion files: `_filter.go`, `_order_field.go`, `_type.go`, `_state.go`, `_status.go` -- Service files: `_service.go` -- Error files: `errors.go` per package -- Test files: `_test.go` co-located +## 16. Compile-time interface assertions -### Generated files -- never edit (universal) +Add at the bottom of every constructor file that implements an +interface: -| File | Generator | Trigger | -|------|----------|---------| -| `pkg/server/api/*/schema/schema.go` | gqlgen | `go generate ./pkg/server/api/*/v1` | -| `pkg/server/api/*/types/types.go` | gqlgen | `go generate ./pkg/server/api/*/v1` | -| `pkg/server/api/mcp/v1/server/server.go` | mcpgen | `go generate ./pkg/server/api/mcp/v1` | - -## ISC License Header - -Every source file must start with the ISC license header. Use the current year for new files. When editing an existing file with a different year, update to a range (e.g., `2025-2026`). Never overwrite the original year. See [shared.md -- License](../shared.md#license) and `contrib/claude/license.md` for templates. - -Outdated copyright years are flagged in review. A dedicated PR (#930, 1572 lines) was merged specifically for bulk copyright header updates. - -## Review-Enforced Standards - -These Go-specific patterns are enforced in code review. See [shared.md -- Review-Enforced Standards](../shared.md#review-enforced-standards) for cross-cutting patterns. - -### High-confidence patterns (frequency 5+) - -- **Error wrapping with "cannot" prefix** -- using "failed to" or bare `return err` without wrapping are flagged as CLAUDE.md violations. (enforced in code review, frequency 5; PRs #845, #921) - -### Patterns with strong enforcement (frequency 2-4) +```go +var _ unit.Configurable = (*Implm)(nil) +var _ unit.Runnable = (*Implm)(nil) +var _ worker.Handler[coredata.Evidence] = (*evidenceDescriptionHandler)(nil) +var _ worker.StaleRecoverer = (*evidenceDescriptionHandler)(nil) +``` -- **var () grouped blocks** -- individual var statements are flagged as style issues. (enforced in code review, frequency 4; PRs #917, #909) -- **One argument per line** -- mixed inline/expanded style is rejected. A dedicated cleanup PR (#866) was merged. (frequency 3; PRs #834, #866) -- **t.Parallel() at all levels** -- missing calls in e2e subtests are flagged as CLAUDE.md violations. (frequency 3; PR #845) -- **JOINs over subqueries** -- subqueries in SELECT are flagged as code smell. (frequency 2; PR #917) -- **String-based enums** -- iota-based enums are rejected. (frequency 2; PR #917) -- **No panic in GraphQL resolvers** -- a cleanup PR (#865, 1425 lines) was merged to eliminate all instances. (frequency 2; PR #865) -- **No speculative indexes** -- indexes without performance justification are rejected. (frequency 2; PR #917) -- **Copyright year current** -- outdated years flagged; bulk update PR #930 merged. (frequency 2; PRs #930, #921) +These cost nothing at runtime and prevent silent interface drift when +a method signature changes. + +--- + +## 17. Review-Enforced Standards (Go-specific subset) + +The full table lives in [shared.md § 13](../shared.md#13-code-review-enforced-standards). +The Go-specific rules consistently enforced in code review: + +| Rule | Ref | +| --- | --- | +| All SQL in `pkg/coredata` (no inline SQL in services / handlers / workers) | shared.md #1 | +| Wrap errors with context (`cannot ...: %w`) — bare `return err` blocks approval | shared.md #2 | +| Remove redundant comments that restate the code | shared.md #3 | +| Use `pkg/baseurl` for application URL construction | shared.md #7 | +| `New*` constructor naming | shared.md #8 | +| No `json` tags on internal structs | shared.md #9 | +| Auth-sensitive packages (OAuth2, OIDC, PKCE, ID-token) need 100% unit test coverage | shared.md #11 | +| Webhook payloads use DTOs from `pkg/webhook/types`, never raw `coredata` structs | shared.md #13 | +| Tests live in `*_test` package, not inside the package under test | shared.md #14 | +| Extract complex `switch`/`case` into private functions | shared.md #17 | +| Use `http.StatusXxx` constants, never bare integers | shared.md #18 | +| Avoid JOINs in `coredata` when two queries are clearer | shared.md #19 | diff --git a/.claude/guidelines/go-backend/index.md b/.claude/guidelines/go-backend/index.md index 49289d2e35..fdf8117a63 100644 --- a/.claude/guidelines/go-backend/index.md +++ b/.claude/guidelines/go-backend/index.md @@ -1,87 +1,148 @@ -# Probo -- Go Backend +# Probo — Go Backend -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) -> Sections wrapped in will be preserved during refresh. +> Auto-generated by potion-skill-generator. Last generated: 2026-05-01. +> Cross-cutting conventions: see [shared.md](../shared.md). +> Sections wrapped in `` are preserved during refresh. +> Authoritative sources are the per-topic guides under `contrib/claude/` — +> when this file disagrees with a `contrib/claude/*.md` doc, the doc wins. ## Architecture Overview -The Go backend is a monolithic service compiled into `bin/probod` that serves all API surfaces (GraphQL, MCP, SCIM, SAML, OIDC), runs background workers (SCIM sync, email sending, webhook delivery, certificate provisioning, evidence description), and embeds the compiled React frontend assets. It is organized as a single Go module (`go.probo.inc/probo`) with a layered architecture enforced by convention rather than framework. - -The layers flow top-down: **HTTP routing** (`pkg/server`) assembles chi routers and middleware chains. **API handlers** (`pkg/server/api/*/v1`) contain GraphQL resolvers (gqlgen), MCP tool resolvers (mcpgen), and protocol handlers (SAML, OIDC, SCIM). **Business logic** (`pkg/probo`, `pkg/iam`, `pkg/trust`) validates requests and orchestrates operations. **Data access** (`pkg/coredata`) is the single location for all raw SQL -- no other package may contain SQL queries. **Infrastructure** packages provide cross-cutting capabilities: LLM integration (`pkg/llm`, `pkg/agent`), background workers (`pkg/webhook`, `pkg/mailer`, `pkg/certmanager`), and security primitives (`pkg/securecookie`, `pkg/securetoken`). - -Every feature must be exposed through all three interfaces (GraphQL, MCP, CLI) and backed by end-to-end tests in `e2e/`. See [shared.md -- Three-interface API surface rule](../shared.md#cross-stack-contracts) for the full contract. +The Go backend is a single-binary daemon (`probod`) that serves the entire +backend runtime: a chi-based HTTP server hosting four GraphQL APIs (console, +trust, connect) plus an MCP API, a fleet of poll-based background workers +(mailer, slack, esign, evidence describer, export jobs, custom-domain ACME), +the Trust Center HTTP/HTTPS server with dynamic TLS, and an outbound +webhook delivery loop. A second binary (`prb`) is the operator CLI; a third +(`probod-bootstrap`) generates the YAML config from environment variables +inside the Docker entrypoint. + +The package layout is **flat by intention**: `pkg/probod` is the only +composition root, `pkg/probo` is the only domain-service layer +(`Service` → `Service.WithTenant` → `TenantService` with one sub-service +field per entity), and `pkg/coredata` is the only place SQL is allowed +to exist. Authorization is policy-as-Go-code in `pkg/iam/policy` with +policies registered at startup; resolvers (GraphQL and MCP) call +`r.authorize(ctx, id, action)` as their first line. Tenant isolation is +enforced by the `coredata.Scoper` injected into every query — see +[shared.md § 9](../shared.md#9-tenant-isolation--cross-stack-architectural-principle). + +The wiring philosophy is **constructor injection, no DI container**: every +service receives its dependencies as positional parameters from +`pkg/probod/probod.go:Run()`. Workers use `go.gearno.de/kit/worker` with +PostgreSQL `FOR UPDATE SKIP LOCKED` claim queries. LLM access goes through +`pkg/llm`, agent orchestration through `pkg/agent`, and outbound HTTP +through `go.gearno.de/kit/httpclient` with SSRF protection on by default. ## Module Map | Module | Purpose | Key Patterns | -|--------|---------|-------------| -| `pkg/gid` | 192-bit tenant-scoped entity identifiers | Panic on crypto failure; base64url serialization; entity type registry in coredata | -| `pkg/coredata` | All raw SQL, entity structs, filters, order fields, migrations | Scoper interface, StrictNamedArgs, static SQL fragments, no ORM | -| `pkg/validator` | Fluent validation framework | Check/CheckEach API, SafeText composite, nil = optional | -| `pkg/probo` | Core business logic (40+ domain sub-services) | Service / TenantService two-level tree, request validation, webhook in same tx | -| `pkg/iam` | Authentication and authorization | ABAC policy evaluator, session model, SAML/OIDC/SCIM sub-modules | -| `pkg/iam/policy` | Pure in-memory IAM policy evaluation | Deny-wins precedence, wildcard action matching, ABAC conditions | -| `pkg/iam/saml` | SAML 2.0 SP implementation | Replay detection, GC worker, transaction-wrapped assertion handling | -| `pkg/iam/oidc` | OIDC federated sign-in (Google, Microsoft) | PKCE flow, JWKS caching, enterprise-only enforcement | -| `pkg/iam/scim` | SCIM 2.0 inbound provisioning + outbound bridge | FOR UPDATE SKIP LOCKED, exponential backoff, bridge state machine | -| `pkg/trust` | Public trust center service layer | TenantService scoping, visibility gating, NDA/PDF export | -| `pkg/llm` | Provider-agnostic LLM abstraction | Hexagonal adapters (Anthropic/OpenAI/Bedrock), OTel GenAI tracing | -| `pkg/agent` | LLM agent orchestration framework | Functional options, tool dispatch, guardrails, streaming | -| `pkg/agent/guardrail` | Concrete guardrail implementations | Prompt injection (LLM classifier), sensitive data (regex), system prompt leak | -| `pkg/agents` | Domain-specific LLM agents | Changelog generator, vendor assessor | -| `pkg/server` | Top-level HTTP server and router assembly | chi router, CORS/CSRF/security headers, SPA serving | -| `pkg/server/api/console/v1` | Console GraphQL API (gqlgen) | Schema-first codegen, dataloaders, Relay cursor pagination | -| `pkg/server/api/connect/v1` | IAM GraphQL API + SAML/OIDC/SCIM handlers | Session management, SSO protocol handlers | -| `pkg/server/api/trust/v1` | Trust center GraphQL API | @nda directive, magic link auth, visibility gating | -| `pkg/server/api/mcp/v1` | MCP API (mcpgen) | specification.yaml schema-first, MustAuthorize + panic recovery | -| `pkg/server/api/authn` | Authentication middleware | Session cookie, API key Bearer, identity presence guard | -| `pkg/server/api/authz` | Authorization adapter | AuthorizeFunc closure, IAM-to-GraphQL error mapping | -| `pkg/server/api/compliancepage` | Trust center resolution middleware | SNI routing, slug/GID resolution, member provisioning | -| `pkg/server/api/files/v1` | Public file download handler | S3 presigned URL redirect | -| `pkg/cmd` | `prb` CLI (cobra) | Feature-slice layout, GraphQL queries as const strings | -| `pkg/cli` | CLI infrastructure (GraphQL client, config) | api.Client, Paginate[T], YAML config | -| `e2e` | End-to-end integration tests | Factory builders, RBAC testing, tenant isolation | -| `pkg/` (utilities) | Cross-cutting: webhook, mailer, certmanager, slack, connector, bootstrap | Poll-based workers, HMAC signing, env-driven config | +| --- | --- | --- | +| `pkg/coredata` | Sole owner of SQL: entity files, Scoper, filters, order fields, migrations | `Scoper`, `pgx.StrictNamedArgs`, `fmt.Sprintf` template + `maps.Copy` args, `FOR UPDATE SKIP LOCKED` | +| `pkg/gid` | 24-byte tenant-scoped global IDs; `entity_type_reg.go` uint16 registry | `gid.New(tenantID, EntityType)`, base64url wire format | +| `pkg/iam/policy` | Policy / Statement / Condition value objects + Evaluator | `Allow().WithResources().When()`, explicit-deny-wins | +| `pkg/iam` | Authorizer + PolicySet + IAM action constants + `AuthorizationAttributer` | `authorize(ctx, id, action)`, organization-scoped condition | +| `pkg/iam/{oidc,saml,scim}` | OIDC, SAML, SCIM identity provider integrations | Provider-specific flows; SSRF-protected HTTP | +| `pkg/probo` | Domain services (`Service`/`TenantService`); workers (export); IAM `actions.go`/`policies.go` | `Request + Validate()`, `pg.WithTx`, double-pointer optional updates | +| `pkg/server/api/{console,trust,connect}/v1` | gqlgen GraphQL APIs (one schema per surface) | DataLoader middleware, `extend type Mutation` only, error switch w/ default | +| `pkg/server/api/mcp/v1` | mcpgen MCP API + `specification.yaml` | `MustAuthorize`, `Omittable[T]` for null vs absent, panic on unexpected | +| `pkg/server/api/cookiebanner` | Public cookie-banner HTTP endpoints | chi router, JSON envelopes | +| `pkg/server/gqlutils` | Typed GraphQL error helpers (`NotFound`, `Forbidden`, `Internal`, …) | Single `Internal(ctx)` catch-all with extensions.code | +| `pkg/agent` | Agent orchestration framework: `coreLoop`, `FunctionTool`, `Handoff`, `Checkpointer` | LLM tool-call loop with policy-driven tool execution | +| `pkg/agent/tools/{browser,search,security}` | Built-in agent tools | Tool registration via `FunctionTool` | +| `pkg/llm` | Provider-agnostic LLM client (Anthropic, OpenAI, Bedrock) + OTel + model registry | `Client.ChatCompletion[Stream]`, typed sentinel errors | +| `pkg/validator` | Fluent in-memory validation framework | `validator.New(); v.Check(field, name, fns...); v.Error()` | +| `pkg/evidencedescriber` | One-shot LLM wrapper that describes evidence files | `go:embed` system prompt + `llm.FilePart` | +| `pkg/accessreview` | Access-review provider drivers (15+ SaaS apps) | switch-based driver registry | +| `pkg/connector` | OAuth2 / API-key 3rd-party connector framework | `Connector{Initiate,Complete}`, stateless HMAC state token, three `TokenEndpointAuth` variants | +| `pkg/esign` | In-house e-signature workers | poll workers, doc rendering | +| `pkg/docgen` | PDF rendering (chromedp + html2pdf) | headless browser pipeline | +| `pkg/cookiebanner` | Cookie banner domain logic | category/state machine | +| `pkg/trust` | Trust center domain (visibility model) | per-document visibility scopes | +| `pkg/slack` | Slack integration + outbox sender | poll worker on `slack_messages` | +| `pkg/webhook` | Outbox event emitter + HMAC-signed delivery worker | `webhook.InsertData` inside `pg.WithTx`; DTOs in `pkg/webhook/types` | +| `pkg/bootstrap` | Env-vars → `probodconfig.FullConfig` YAML generator (NOT a DB seeder) | `Builder.Build`, `WriteConfig` | +| `pkg/{mail,mailer,mailman}` | Mail outbox, list management, transactional senders | poll worker on `emails` table | +| `pkg/filemanager`, `pkg/filevalidation` | S3/SeaweedFS storage + MIME-type guards | `GetFileBase64`, validation | +| `pkg/page` | Generic Relay-style cursor pagination | `Cursor[T]`, `Page[T,O]`, `CursorKey` | +| `pkg/certmanager` | ACME + custom-domain TLS | autocert-style provisioner + renewer | +| `pkg/crypto/{cipher,passwdhash,rand,hash,keys,pem}` | Crypto primitives | AES-256-GCM, PBKDF2, SHA-256 | +| `pkg/cli` + `pkg/cmd` | `prb` CLI: cobra commands, `cmdutil.Factory`, GraphQL client, `huh` prompts | leaf-command pattern (one file per verb) | +| `pkg/probod` | Composition root: `Implm.Run()`, all goroutines + graceful shutdown | `sync.WaitGroup` + `context.WithCancelCause`, `context.Background()` child contexts | +| `cmd/{probod,prb,probod-bootstrap}` | Binary entry points (3 binaries) | thin `main.go`, delegates to `pkg/*` | +| `internal/cmd/genmodels` | Codegen for `pkg/llm/registry_gen.go` | sources OpenRouter capability data | +| `e2e/` | E2E test suite (~65 files) against live `probod` + Docker Compose | factory builders, RBAC matrix tests, Mailpit, mandatory `t.Parallel()` | + +> **Gap:** `pkg-vetting` profile reports the package does not exist in the +> current worktree. If you find it later, regenerate this index. Vendor- +> assessor wiring lives in `pkg/probod/vendor_assessor.go` +> (returns `probo.DisabledVendorAssessor` when no provider configured). ## Canonical Examples | File | What it demonstrates | -|------|---------------------| -| `pkg/coredata/asset.go` | Complete coredata entity: LoadByID, Insert, Update, Delete, CursorKey, AuthorizationAttributes, Snapshot | -| `pkg/probo/vendor_service.go` | Service layer pattern: Request struct, Validate(), pg.WithTx, webhook in same tx | -| `pkg/server/api/console/v1/v1_resolver.go` | GraphQL resolver pattern: authorize, ProboService, service call, type mapping | -| `pkg/iam/policy/example_test.go` | Policy DSL: Allow/Deny helpers, ABAC conditions, Go example tests | -| `pkg/agent/guardrail/sensitive_data_test.go` | Testing: t.Parallel at all levels, table-driven, require/assert split | -| `e2e/console/vendor_test.go` | E2E test: factory builders, RBAC, tenant isolation, timestamp assertions | +| --- | --- | +| `pkg/coredata/cookie_banner.go` | Full entity pattern: struct, `CursorKey`, `AuthorizationAttributes`, scope+filter+cursor query, `Insert` with `scope.GetTenantID()`, `Update` with `Exec`+`RowsAffected`, FOR UPDATE SKIP LOCKED, unique-constraint-to-`ErrResourceAlreadyExists` | +| `pkg/probo/vendor_service.go` | `Request + Validate()` pattern, `pg.WithTx` with `webhook.InsertData` inside the same tx, double-pointer (`**string`) optional fields on update requests | +| `pkg/probo/evidence_description_worker.go` | Worker pattern: `Claim` (FOR UPDATE SKIP LOCKED, returns `worker.ErrNoTask`), `Process`, `RecoverStale` (5-min default), explicit fail-path with `failEvidence` | +| `pkg/server/api/console/v1/vendor_resolvers.go` | Resolver shape: `r.authorize(ctx, id, action)` first line, error switch with mandatory `default:` → `gqlutils.Internal(ctx)`, DataLoader use, `types.NewVendor` mapping | +| `pkg/probod/probod.go` | Composition root: `migrator.NewMigrator` synchronous, `wg.Go` per subsystem, `cancel(fmt.Errorf("X crashed: %w", err))`, ordered `stop*()` calls before `wg.Wait()`, `pgClient.Close()` last | +| `pkg/connector/oauth2.go` | OAuth2 flow with HMAC-signed stateless `state` token, three `TokenEndpointAuth` modes, SSRF-protected `oauth2Transport` | ## Topic Index -- [Patterns](./patterns.md) -- error handling, data access, authorization, DI, observability -- [Conventions](./conventions.md) -- naming, code style, imports, project structure -- [Testing](./testing.md) -- framework, organization, naming, utilities, example test -- [Pitfalls](./pitfalls.md) -- stack-specific pitfalls and anti-patterns - -## Module Notes - -Modules with distinctive patterns that differ from stack-wide conventions: - -- [pkg/coredata](./module-notes/pkg-coredata.md) -- data access layer specifics -- [pkg/server/api](./module-notes/pkg-server-api.md) -- GraphQL/MCP resolver patterns -- [pkg/cmd](./module-notes/pkg-cmd.md) -- CLI command structure -- [pkg/llm and pkg/agent](./module-notes/pkg-llm-agent.md) -- LLM integration patterns +- [Patterns](./patterns.md) — Service/TenantService, Request+Validate, worker + pattern, authorization, SQL composition, GraphQL/MCP/CLI shapes, outbox, + codegen, OAuth2 connectors, driver pattern. +- [Conventions](./conventions.md) — naming (`New*`), imports, error wrapping, + status constants, struct-tag minimalism, switch extraction, package layout, + test packages, parallel testing, `require` vs `assert`. +- [Testing](./testing.md) — unit testing with testify, e2e factory builders + + Mailpit, RBAC matrix tests, tenant-isolation pattern, pagination + assertions, 100% coverage requirement for auth-sensitive code. +- [Pitfalls](./pitfalls.md) — concrete file-level pitfalls and active drift, + including documented-but-missing helpers, hardcoded SQL literals, TOCTOU + in agent tools, missing SSRF guards, and the 4-file checklist for new + entities. +- [Module notes](./module-notes/) — one short reference per module covering + purpose, key types/files, how to extend, and top pitfalls. + +## Cross-Cutting References + +This file does not duplicate cross-stack rules. See `shared.md` for: + +- [API surface synchronization (GraphQL/MCP/CLI/n8n)](../shared.md#3-the-four-surface-api-rule) +- [Configuration propagation (11 files)](../shared.md#4-configuration-propagation) +- [Git, commits, releases](../shared.md#5-git--workflow) +- [License headers](../shared.md#6-license-headers--isc-on-every-source-file) +- [CI gates](../shared.md#7-ci--quality-gates) +- [Logging PII rules](../shared.md#8-logging-principles-cross-stack) +- [Tenant isolation principle](../shared.md#9-tenant-isolation--cross-stack-architectural-principle) +- [GID layout & encoding](../shared.md#10-global-identifiers-gid--crossing-the-go--ts-boundary) +- [Cross-stack error principles](../shared.md#11-error-handling-principles) +- [Security baseline (SSRF, URL construction, secrets, UUIDs)](../shared.md#12-security-baseline-cross-stack) +- [Code-review-enforced standards](../shared.md#13-code-review-enforced-standards) ## Team Notes -_Reserved for manual additions by the team -- this section is preserved on refresh._ +_Reserved for manual additions by the team — this section is preserved on refresh._ ## Open Questions -- `pkg/coredata/asset_filter.go` uses `pgx.NamedArgs` while `vendor_filter.go` uses `pgx.StrictNamedArgs` -- is this a migration in progress toward StrictNamedArgs everywhere? -- `pkg/trust/service.go` declares a `proboSvc` field on both `Service` and `TenantService` but `NewService` never accepts it as a parameter -- is this a wiring oversight? -- Several modules have no unit tests (pkg/iam/oidc, pkg/trust, pkg/server/api/authn) -- is coverage exclusively via e2e tests, or are gaps to be filled? -- The `Vendor` struct in coredata has a `TenantID` field unlike all other entities -- is this intentional legacy or should it be removed? +- `pkg/coredata/agent_run.go:472` hardcodes `'PENDING'` as a SQL string + literal. Confirm whether this is being left for a focused refactor or + should be fixed opportunistically next time the file is touched. +- `pkg/iam/policy` documentation references `In()` / `NotIn()` builder + helpers that do not exist in the source — confirm with the IAM owner + whether the docs should be corrected or the helpers added. +- `pkg/probod/probod.go` `runTrustCenterServer` uses `errgroup.WithContext` + while every other subsystem uses `sync.WaitGroup` + `cancel`. Documented + as intentional; confirm the rationale (errgroup-on-fail-fast for the + cert provisioner/renewer pair). +- `pkg-vetting` package is referenced in the module list but does not + appear in the current worktree. Confirm whether it was renamed/folded + into `pkg/probo` or is upcoming. diff --git a/.claude/guidelines/go-backend/module-notes/accessreview.md b/.claude/guidelines/go-backend/module-notes/accessreview.md new file mode 100644 index 0000000000..134239e60b --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/accessreview.md @@ -0,0 +1,40 @@ +# Probo — Go Backend — pkg/accessreview + +**Purpose.** Provider-driver registry for access-review integrations +across 15+ SaaS apps (GitHub, Okta, Google Workspace, Slack, AWS, +Linear, Notion, ...). Each driver implements the `Driver` interface +(list users, list group memberships, etc.) backed by a +`connector.Connection`. + +**Key files.** + +- `pkg/accessreview/driver.go` — `Driver` interface, **switch-based** + `NewDriver(provider, conn) (Driver, error)` registry. +- `pkg/accessreview//` — one sub-package per provider. + +**How to extend (a new provider).** + +1. Create `pkg/accessreview//driver.go` implementing the + `Driver` interface. +2. Add a `case` to `NewDriver` in `pkg/accessreview/driver.go`. +3. Register the provider in `pkg/connector/providers.go` (OAuth2 URLs + and probe URL). +4. Add it to the GraphQL/MCP enum exposing connector providers. + +**Why switch-based instead of init-side-effect registration?** + +The `accessreview` package deliberately uses an explicit switch instead +of `init()` registration. The reason is *auditable diff*: adding a new +provider is a one-line `case` addition that reviewers can spot on +sight, with no hidden ordering or import-side-effect surprises. The +only place we use `init`-style registration is `pkg/connector`, and +only because it must wire deployment-supplied client IDs/secrets at +startup. + +**Top pitfalls.** + +- Forgetting the `case` in `NewDriver` after adding the sub-package — + runtime "unsupported provider" error at first use. The build won't + catch this. +- Skipping the connector registry edit — the OAuth2 flow has nowhere + to go. See [pitfalls.md § 15](../pitfalls.md). diff --git a/.claude/guidelines/go-backend/module-notes/agent.md b/.claude/guidelines/go-backend/module-notes/agent.md new file mode 100644 index 0000000000..dd7d3bd2ae --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/agent.md @@ -0,0 +1,42 @@ +# Probo — Go Backend — pkg/agent (and tools sub-packages) + +**Purpose.** In-house agent orchestration framework. `pkg/agent` provides +the `coreLoop` (LLM tool-call iteration), `FunctionTool` registration, +`Handoff` between agents, and `Checkpointer` (pluggable; the production +implementation is `coredata.PGCheckpointer`). +`pkg/agent/tools/{browser,search,security}` are first-party tool +implementations. + +**Key files.** + +- `pkg/agent/agent.go` — `Agent`, `New`, `Run`, `coreLoop`. +- `pkg/agent/tool.go` — `Tool` interface, `FunctionTool` adapter. +- `pkg/agent/handoff.go` — agent-to-agent handoff plumbing. +- `pkg/agent/checkpoint.go` — `Checkpoint` and `Checkpointer` interfaces; + PGCheckpointer lives in `pkg/coredata/agent_run.go`. +- `pkg/agent/tools/browser/` — chromedp-backed navigation tools + (`NavigateToURLTool`, screenshot, click, ...). +- `pkg/agent/tools/search/` — web search tool. +- `pkg/agent/tools/security/csp.go` — CSP-header validator tool. + +**How to extend (a new tool).** + +1. Create a struct that implements `agent.Tool` (or build a + `FunctionTool` from a typed `func(ctx, In) (Out, error)`). +2. Register the tool with the agent at construction time. +3. If the tool dials user-influenced URLs, **use + `httpclient.WithSSRFProtection()`** and validate the URL with + `netcheck.ValidatePublicURL` before dispatch. Disable redirects on + the http client to avoid TOCTOU (or rely on httpclient's per-dial + guard). + +**Top pitfalls.** + +- `pkg/agent/tools/search` uses bare `http.Client` — SSRF gap. See + [pitfalls.md § 4](../pitfalls.md). +- `pkg/agent/tools/security/csp.go` skips + `netcheck.ValidatePublicURL`. See [pitfalls.md § 5](../pitfalls.md). +- `NavigateToURLTool` follows redirects after validation — TOCTOU. See + [pitfalls.md § 6](../pitfalls.md). +- Long-running tool calls must respect ctx cancellation — the worker + driving the agent already passes a deadline. diff --git a/.claude/guidelines/go-backend/module-notes/certmanager.md b/.claude/guidelines/go-backend/module-notes/certmanager.md new file mode 100644 index 0000000000..3abf5270e5 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/certmanager.md @@ -0,0 +1,31 @@ +# Probo — Go Backend — pkg/certmanager + +**Purpose.** ACME (Let's Encrypt / Pebble) + custom-domain TLS for the +Trust Center server. Provides dynamic `GetCertificate` for +`tls.Config`, an HTTP-01 challenge handler, and background provisioner ++ renewer goroutines. + +**Key files.** + +- `pkg/certmanager/service.go` — `Service`, `GetCertificate`, ACME + challenge handling. +- `pkg/certmanager/provisioner.go` — background loop that issues new + certs for newly-added custom domains. +- `pkg/certmanager/renewer.go` — background loop that renews certs + before expiry. +- `pkg/coredata/custom_domain*.go` — domain rows + cert storage. + +**How to use.** Wired in `pkg/probod/probod.go` `runTrustCenterServer`, +which is the **only** subsystem using `errgroup.WithContext` (cert +provisioner, renewer, HTTP server, HTTPS server form a unit — see +[probod.md](./probod.md) and +[pitfalls.md § 17](../pitfalls.md)). + +**Top pitfalls.** + +- ACME rate limits — back off aggressively on failures; the renewer + must respect the upstream's `Retry-After`. +- Pebble (dev ACME) returns a different cert chain than Let's Encrypt; + e2e tests against Pebble must trust the Pebble root. +- Don't add the cert subsystems to the top-level `sync.WaitGroup` — keep + them inside `runTrustCenterServer`'s errgroup so they fail together. diff --git a/.claude/guidelines/go-backend/module-notes/cli.md b/.claude/guidelines/go-backend/module-notes/cli.md new file mode 100644 index 0000000000..99163fa4f0 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/cli.md @@ -0,0 +1,48 @@ +# Probo — Go Backend — pkg/cli + pkg/cmd + +**Purpose.** The `prb` CLI command tree. `pkg/cli` provides the GraphQL +HTTP client, generic cursor-pagination helper, OAuth2 / device-flow +auth, and YAML config (`~/.config/prb/config.yaml`). `pkg/cmd` is the +cobra command tree — one directory per resource, one file per verb. + +> See [patterns.md § 9 CLI command shape](../patterns.md#9-cli-command-shape). + +**Key files.** + +- `pkg/cli/api/client.go` — GraphQL client (Bearer auth, 401 + auto-refresh, multipart uploads). +- `pkg/cli/api/pagination.go` — `api.Paginate[T]` generic cursor walker. +- `pkg/cli/config/config.go` — YAML config (hosts → tokens), env-var + overrides (`PROBO_HOST`, `PROBO_TOKEN`). +- `pkg/cmd/cmdutil/factory.go` — `Factory` (DI container: IOStreams, + Version, lazy Config). +- `pkg/cmd/cmdutil/table.go` — `NewTable` (lipgloss/table for list + output). +- `pkg/cmd/iostreams/iostreams.go` — TTY/CI/NO_COLOR detection. +- `pkg/cmd//.go` — group command wiring its verbs. +- `pkg/cmd///.go` — leaf command (one per file). +- `pkg/cmd/risk/list/list.go` — canonical leaf command. +- `pkg/cmd/root/root.go` — root command, registers all groups. + +**How to extend (a new verb).** + +1. Create `pkg/cmd///.go` with: + - `const Query/Mutation = "..."` GraphQL string. + - Unexported `Response` struct. + - `func NewCmd(f *cmdutil.Factory) *cobra.Command`. +2. Wire it from `pkg/cmd//.go`. +3. Output: tables to `f.IOStreams.Out`, info/truncation to + `f.IOStreams.ErrOut`. +4. Interactive prompts via `huh`, gated on `IOStreams.IsInteractive()`. + +**Top pitfalls.** + +- Calling `huh` without checking `IsInteractive()` — breaks CI / pipes / + `--no-interactive`. +- Hand-rolling pagination — use `api.Paginate[T]` so cursor handling + stays consistent across verbs. +- Bypassing the `Factory` and reading config directly — breaks tests + that inject a fake `Config` via `Factory`. +- Coupling the leaf to a specific output format — keep table vs JSON + selection in the leaf, controlled by the global `--output` flag from + `cmdutil`. diff --git a/.claude/guidelines/go-backend/module-notes/connector.md b/.claude/guidelines/go-backend/module-notes/connector.md new file mode 100644 index 0000000000..218a8610fd --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/connector.md @@ -0,0 +1,57 @@ +# Probo — Go Backend — pkg/connector + +**Purpose.** Generic 3rd-party SaaS connector framework. Provides +OAuth2 authorization-code, OAuth2 client-credentials, and API-key +authentication. Centralises HMAC-signed stateless `state` token, +SSRF-protected HTTP transport, and per-provider config (URLs, scopes, +auth mode). + +> See [patterns.md § 12](../patterns.md#12-connector-oauth2-framework). + +**Key files.** + +- `connector.go` — `Connector` interface (`Initiate`, `Complete`), + `Connection` interface (`Type`, `Client`, `Scopes`, + Marshal/Unmarshal). +- `oauth2.go` — `OAuth2Connector`, `OAuth2Connection`, + `oauth2Transport` (Bearer-token round tripper), + `RefreshableClient`, `clientCredentialsClient`. +- `apikey.go` — `APIKeyConnection` (static Bearer key). +- `slack.go` — `SlackConnection` (provider-specific extension). +- `providers.go` — `providerDefinitions` map (SLACK, GITHUB, + GOOGLE_WORKSPACE, LINEAR, HUBSPOT, DOCUSIGN, NOTION, SENTRY, + INTERCOM, BREX, CLOUDFLARE, OPENAI, SUPABASE, TALLY, RESEND, + ONE_PASSWORD) + `ApplyProviderDefaults`. +- `registry.go` — `ConnectorRegistry` (thread-safe map; OAuth2 probe + URLs). +- `scopes.go` — `ParseScopeString`, `FormatScopeString`, `UnionScopes`. + +**Three `TokenEndpointAuth` modes** (chosen per provider in +`providerDefinitions`): + +- `post-form` — credentials in form body (most providers). +- `basic-form` — Basic auth header + form body. +- `basic-json` — Basic auth header + JSON body (Notion). + +**How to extend (a new OAuth2 provider).** + +1. `pkg/connector/providers.go` — add the entry to + `providerDefinitions` (AuthURL, TokenURL, ExtraAuthParams, + TokenEndpointAuth, SupportsIncrementalAuth). +2. `pkg/probod/probod.go` — call + `ApplyProviderDefaults(connector, providerName)` then + `registry.Register(providerName, connector)` with the deployment + client ID/secret. +3. GraphQL/MCP enum — add the provider name to the four-surface enum so + clients can request it (see + [shared.md § 3](../../shared.md#3-the-four-surface-api-rule)). + +**Top pitfalls.** + +- `OAuth2Connector.HTTPClient` is required and must be SSRF-protected. + `ApplyProviderDefaults` injects it; tests must inject + `httpclient.DefaultClient(WithSSRFProtection(), WithSSRFAllowLoopback())`. +- Three-map edit is easy to half-do — see + [pitfalls.md § 15](../pitfalls.md). +- `state` token must round-trip safely; never store side-data in a DB + for OAuth2 state (the framework intentionally avoids that). diff --git a/.claude/guidelines/go-backend/module-notes/coredata.md b/.claude/guidelines/go-backend/module-notes/coredata.md new file mode 100644 index 0000000000..5ddedeb7de --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/coredata.md @@ -0,0 +1,45 @@ +# Probo — Go Backend — pkg/coredata + +**Purpose.** Sole owner of SQL in the codebase. One file per entity +(`vendor.go`, `cookie_banner.go`, ...) plus a paired +`_filter.go` and `_order_field.go`. Houses the +`Scoper` interface (tenant isolation), 300+ migrations under +`pkg/coredata/migrations/`, and the entity-type registry that backs +`pkg/gid`. Implements `agent.Checkpointer` via `PGCheckpointer`. + +**Key files.** + +- `scope.go` — `Scoper` interface (`SQLFragment`, `SQLArguments`, + `GetTenantID`); `Scope` (multi-tenant) and `NoScope` (cross-tenant + admin, panics on `GetTenantID`). +- `entity_type_reg.go` — sequential `uint16` constants and + `NewEntityFromID`; never reuse a removed number (use `_` placeholder). +- `errors.go` — `ErrResourceNotFound`, `ErrResourceAlreadyExists`, + `ErrResourceInUse`, `ErrNoDocumentPDFJobAvailable`. +- `cookie_banner.go` — canonical entity (full CRUD + cursor + filter + + worker query). +- `agent_run.go` — `PGCheckpointer`; **deviation**: line 472 hardcodes + `'PENDING'` SQL literal (drift to fix). +- `migrations.go` — `embed.FS` of all migration SQL. + +**How to extend (add a new entity).** + +1. Create `pkg/coredata/.go` — struct with `db` tags, `gid.GID` + ID, `OrganizationID`, timestamps, slice alias `s`, + `AuthorizationAttributes()`, `CursorKey()`, CRUD methods. +2. Create `_filter.go` and `_order_field.go`. +3. Append a sequential constant to `entity_type_reg.go`; add a `case` + in `NewEntityFromID`. +4. Add a migration file under `migrations/` named + `YYYYMMDDTHHMMSSZ.sql` with **random 6-digit time portion** to avoid + collisions (see [shared.md § 5 memory note](../../shared.md#5-git--workflow)). +5. Run `make test` to confirm migrations apply on a fresh DB. + +**Top pitfalls.** + +- Forgetting to declare every key in `pgx.StrictNamedArgs` (filter args + must be present even when nil) — see + [pitfalls.md](../pitfalls.md). +- Calling `scope.GetTenantID()` on `NoScope` — runtime panic. +- Reusing a removed entity-type number — corrupts every existing GID + for that type. diff --git a/.claude/guidelines/go-backend/module-notes/e2e.md b/.claude/guidelines/go-backend/module-notes/e2e.md new file mode 100644 index 0000000000..4900b315b5 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/e2e.md @@ -0,0 +1,60 @@ +# Probo — Go Backend — e2e/ + +**Purpose.** End-to-end test suite (~65 files) running against a live +`probod` binary backed by Docker Compose (Postgres, Mailpit, SeaweedFS, +optionally Pebble ACME, Keycloak). Exercises the full GraphQL surface +(console + connect + mcp), authentication flows, RBAC matrix, and +tenant isolation. + +> See [testing.md § 3 E2E mechanics](../testing.md#3-e2e-mechanics). + +**Key files.** + +- `e2e/internal/testutil/client.go` — `Client` (cookie-jar HTTP + transport, `Execute`, `Do`, `ExecuteConnect`, `ExecuteWithFile`, + `ExecuteShouldFail`). +- `e2e/internal/testutil/graphql.go` — `GraphQLErrors`, transport. +- `e2e/internal/testutil/assert.go` — assertion helpers + (`RequireForbiddenError`, `RequireErrorCode`, `AssertNodeNotAccessible`, + `AssertFirstPage`/`AssertMiddlePage`/`AssertLastPage`, + `AssertOrderedAscending`/`AssertTimesOrderedDescending`, + `AssertTimestampsOnCreate`/`AssertTimestampsOnUpdate`). +- `e2e/internal/testutil/mailpit.go` — `SearchMails`, + `CheckMessageLinks`. +- `e2e/internal/testutil/env.go` — `GetBaseURL`, `GetMailpitBaseURL`. +- `e2e/internal/factory/.go` — flat `CreateXxx(c, factory.Attrs{...})` + + builder `NewXxx(c).WithField(...).Create()`. +- `e2e/internal/factory/factory.go` — `Attrs`, `SafeName`, `SafeEmail`, + `SafeOrigin` (gofakeit-backed). +- `e2e/console/_test.go` — one file per entity. `vendor_test.go` + is the canonical reference. +- `e2e/mcp/_test.go` — MCP tool tests. + +**How to extend (a new e2e test).** + +1. Add factory entries under `e2e/internal/factory/.go` (both + the flat and builder forms). +2. Create `e2e/console/_test.go` (or `e2e/mcp/`) with + `package console_test`. +3. Top-level test functions and **every** `t.Run(...)` subtest call + `t.Parallel()`. +4. Cover at minimum: happy path, RBAC matrix (all 5 roles × all + verbs), tenant isolation (two `NewClient` calls + `AssertNodeNotAccessible`), + pagination (use `AssertFirstPage` etc.), and timestamps on + create/update. +5. For email side effects, poll Mailpit via + `c.SearchMails(t, ctx, "to:"+email)`. + +**Top pitfalls.** + +- Missing `t.Parallel()` at any subtest level — serialises the suite, + called out in review. +- Sharing factory output across tests by package-level vars — every + test creates its own org via `NewClient`; cross-test sharing breaks + isolation. +- Hard-coded names/emails — collisions with the parallel suite. Use + `factory.SafeName` / `factory.SafeEmail`. +- Asserting on Mailpit immediately without polling — mail delivery is + async; allow a few retries. +- Forgetting RBAC + tenant-isolation tests on a new mutation — these + are the project's safety net for the `Scoper` invariant. diff --git a/.claude/guidelines/go-backend/module-notes/esign.md b/.claude/guidelines/go-backend/module-notes/esign.md new file mode 100644 index 0000000000..cc249c8f92 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/esign.md @@ -0,0 +1,31 @@ +# Probo — Go Backend — pkg/esign + +**Purpose.** In-house e-signature workflow. Renders signing-ready PDFs +via `pkg/docgen` (chromedp + html2pdf), sends signing requests by +email, captures signatures, and emits webhook events at each stage. +Orchestrated by poll-based workers using the standard +`kit/worker` + FOR UPDATE SKIP LOCKED pattern. + +**Key files.** + +- `pkg/esign/service.go` — `Service`, `Sign`, `Cancel`, `LoadByID`. +- `pkg/esign/.go` — render worker, dispatch worker, reminder + worker (each implements `worker.Handler`). +- `pkg/coredata/esignature*.go` — entity rows + status enum. + +**How to extend.** Add a new state to the signature workflow: + +1. Extend the status enum in `pkg/coredata`. +2. Add a migration that allows the new state. +3. If a new worker is needed, follow the + [worker pattern](../patterns.md#3-worker-pattern-poll-based--for-update-skip-locked). +4. Update the `pkg/probo/document_service.go` error mapping if the + custom errors change. + +**Top pitfalls.** + +- Document service custom errors (`ErrSignatureNotCancellable`, + `ErrDocumentVersionNotDraft`, ...) must be mapped at every API + boundary — missing case in resolver → leaks to client. +- Reminder cadence is bound to the dispatcher's poll interval; do not + add a separate timer — extend the dispatcher. diff --git a/.claude/guidelines/go-backend/module-notes/filemanager.md b/.claude/guidelines/go-backend/module-notes/filemanager.md new file mode 100644 index 0000000000..f8b351f3a7 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/filemanager.md @@ -0,0 +1,29 @@ +# Probo — Go Backend — pkg/filemanager + pkg/filevalidation + +**Purpose.** S3-compatible blob storage abstraction (used against +SeaweedFS in dev, S3 in prod) plus MIME-type validation guards. + +**Key files.** + +- `pkg/filemanager/service.go` — `Service`, `Upload`, `Get`, + `GetFileBase64`, `Delete`, presigned URLs. +- `pkg/filevalidation/validate.go` — MIME-type allow-lists per use case + (evidence, document import, logo upload). + +**How to use.** + +- Upload: stream the file body into `Upload`, get back a storage key. +- Read: `Get` for `io.ReadCloser` streaming; + `GetFileBase64` when the consumer needs it as a base64 string (e.g. + the LLM `FilePart` payload — see + [evidencedescriber pattern](./llm.md)). + +**Top pitfalls.** + +- Using a presigned URL with too long a TTL — keep TTLs minutes, not + hours. +- Skipping `pkg/filevalidation` on user-supplied uploads — gives users + a free path to upload arbitrary content. Always validate MIME against + the allow-list relevant to the use case. +- Holding open file handles across goroutine boundaries — close before + returning to the worker loop. diff --git a/.claude/guidelines/go-backend/module-notes/gid.md b/.claude/guidelines/go-backend/module-notes/gid.md new file mode 100644 index 0000000000..859c7899ff --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/gid.md @@ -0,0 +1,29 @@ +# Probo — Go Backend — pkg/gid + +**Purpose.** 24-byte global identifier used everywhere instead of UUIDs. +Embeds the tenant ID, entity-type uint16, timestamp ms, and 6 random +bytes. Wire format is base64url. Implements `sql.Scanner`, +`driver.Valuer`, `MarshalText`, `UnmarshalText` for transparent +PostgreSQL and JSON usage. + +See [shared.md § 10](../../shared.md#10-global-identifiers-gid--crossing-the-go--ts-boundary) +for the layout, wire format, and frontend interop. + +**Key files.** + +- `gid.go` — `GID` type, `New(tenantID, entityType)`, + `NewEntityFromID`, `TenantID()` accessor. +- `tenant.go` — `TenantID` byte-array type with its own (Un)MarshalText. + +**How to extend.** GID layout itself is fixed; extending means adding a +new entity type in `pkg/coredata/entity_type_reg.go` (see +[coredata.md](./coredata.md)). + +**Top pitfalls.** + +- Using `github.com/google/uuid` instead of the GID toolchain. The + project removed `google/uuid` deliberately — use `pkg/crypto/rand` for + raw randomness or `gid.New(...)` for entity IDs. +- Calling `gid.New(tenantID, ...)` from inside `pkg/coredata`. By + convention IDs are minted **in the service layer** (`pkg/probo`) and + passed to `coredata.Insert` already populated. diff --git a/.claude/guidelines/go-backend/module-notes/iam.md b/.claude/guidelines/go-backend/module-notes/iam.md new file mode 100644 index 0000000000..f72ee06023 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/iam.md @@ -0,0 +1,49 @@ +# Probo — Go Backend — pkg/iam (and sub-packages) + +**Purpose.** Identity, authentication, and authorization for the entire +backend. `pkg/iam/policy` is the pure value-object layer (Policy / +Statement / Condition / Evaluator); `pkg/iam` is the orchestrator +(`Authorizer`, `PolicySet`, action constants); `pkg/iam/oidc`, +`pkg/iam/saml`, `pkg/iam/scim`, `pkg/iam/oauth2server` are identity-provider +integrations. + +> See [patterns.md § 4](../patterns.md#4-authorization). + +**Key files.** + +- `pkg/iam/policy/policy.go`, `statement.go`, `evaluator.go` — fluent + policy DSL. Decision order: explicit Deny > explicit Allow > + implicit deny. +- `pkg/iam/iam_actions.go` — IAM action constants (`iam:organization:*`). +- `pkg/iam/iam_policy_set.go` — IAM policies (admin/owner/employee/...). +- `pkg/iam/authorizer.go` — `Authorizer` (loads attributes, membership, + selects policies, evaluates) + `AuthorizationAttributer` interface + (entities implement it to expose attributes). +- `pkg/server/api/authz/authorization.go` — `AuthorizeFunc` factory + used by GraphQL resolvers. + +**How to extend (a new product action).** + +1. Add the action constant to `pkg/probo/actions.go` (or + `pkg/iam/iam_actions.go` for IAM-domain actions). +2. Add the `Allow` statement to `pkg/probo/policies.go` — every product + policy **must** include `organizationCondition()` to scope it to + the principal's org. Without it, the policy leaks across tenants. +3. Call `r.authorize(ctx, resourceID, probo.ActionXxx)` as the first + line of every resolver method that uses the action. +4. Verify with an RBAC matrix e2e test (see + [testing.md § 4](../testing.md#4-test-patterns)). + +**Top pitfalls.** + +- Skipping `organizationCondition` — over-permissive cross-tenant policy. +- Following `contrib/claude/authorization.md` literally and using + `policy.In(...)` — the helper does not exist. Use the `Condition` + struct directly with `ConditionOperator.In`. See + [pitfalls.md § 2](../pitfalls.md). +- Logging the OIDC `error_description` verbatim — PII leak. See + [pitfalls.md § 3](../pitfalls.md). +- Failed PKCE that does not delete the auth code — replay attack + surface (PR #957). See [pitfalls.md § 20](../pitfalls.md). +- 100% unit-test coverage requirement on auth-sensitive code is + enforced in review. diff --git a/.claude/guidelines/go-backend/module-notes/llm.md b/.claude/guidelines/go-backend/module-notes/llm.md new file mode 100644 index 0000000000..b3827782aa --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/llm.md @@ -0,0 +1,39 @@ +# Probo — Go Backend — pkg/llm + +**Purpose.** Provider-agnostic LLM client over Anthropic, OpenAI, and +AWS Bedrock. Wraps each call in OTel spans (GenAI semantic conventions) +and ships a code-generated capability registry sourced from +OpenRouter. + +**Key files.** + +- `provider.go` — `Provider` interface (`ChatCompletion`, + `ChatCompletionStream`). +- `llm.go` — `Client` (instrumented wrapper around `Provider`); + consumers use `Client`, never `Provider` directly. +- `chat.go` — `ChatCompletionRequest/Response`, + `ChatCompletionStream` interface, `StreamAccumulator`. +- `message.go` — `Message`, `Part` (`TextPart`, `ImagePart`, `FilePart`, + `ThinkingPart`). +- `errors.go` — typed sentinels (`ErrRateLimit`, `ErrContextLength`, + `ErrContentFilter`, `ErrAuthentication`, `ErrStreamingRequired`). +- `registry.go` — model capability lookup. +- `registry_gen.go` — generated by `internal/cmd/genmodels`. +- `trace.go` — `tracedStream` ends span exactly once via `sync.Once`. +- `anthropic/`, `openai/`, `bedrock/` — per-provider adapters. + +**How to extend.** + +- New model: regenerate `registry_gen.go` (`go generate ./pkg/llm`) + after refreshing OpenRouter data via `internal/cmd/genmodels`. +- New provider: implement `Provider` in a new sub-package, write a + `mapError` to translate SDK errors to `llm.Err*`, wire it from + `pkg/probod/llm.go`. + +**Top pitfalls.** + +- Anthropic requires `MaxTokens` — without it, returns + `ErrContextLength` immediately. See [pitfalls.md § 12](../pitfalls.md). +- `stream.Close()` must always be called — otherwise the HTTP + connection and OTel span leak. See [pitfalls.md § 13](../pitfalls.md). +- Editing `registry_gen.go` by hand. **Don't.** Re-run the generator. diff --git a/.claude/guidelines/go-backend/module-notes/mailer-and-mailman.md b/.claude/guidelines/go-backend/module-notes/mailer-and-mailman.md new file mode 100644 index 0000000000..73d2efb590 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/mailer-and-mailman.md @@ -0,0 +1,44 @@ +# Probo — Go Backend — pkg/mail + pkg/mailer + pkg/mailman + +**Purpose.** Transactional email outbox and list management. + +| Package | Role | +| --- | --- | +| `pkg/mail` | Email primitives (Address, Message, attachments) and rendering helpers. | +| `pkg/mailer` | Outbox sender — drains the `emails` table via a poll worker and delivers via SMTP / SES / configured transport. | +| `pkg/mailman` | List-management: subscriber lists, unsubscribe tokens, list-unsubscribe headers. | + +**Key files.** + +- `pkg/mail/message.go` — `Message`, `Attachment`. +- `pkg/mailer/sender.go` — poll worker (Claim / Process / RecoverStale). +- `pkg/coredata/email.go` — outbox row. +- `pkg/mailman/service.go` — list management + unsubscribe. +- E2E: `e2e/internal/testutil/mailpit.go` reads delivered mail for + assertions. + +**How to use (canonical send).** Inside a service transaction: + +```go +err := pg.WithTx(ctx, func(tx pg.Tx) error { + // ... entity mutation ... + return mailer.InsertEmail(ctx, tx, scope, mail.Message{ + From: mail.Address{...}, + To: []mail.Address{...}, + Subject: "...", + HTMLBody: rendered, + }) +}) +``` + +The sender drains the row asynchronously. + +**Top pitfalls.** + +- Sending mail outside a transaction → partial-state risk identical to + the webhook outbox. +- Including PII in the subject line that ends up in logs (most senders + log the subject). Keep PII in the body, not the subject. +- Mailpit assertions in e2e need polling — use + `Client.SearchMails` with retries; don't rely on a single immediate + fetch. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-cmd.md b/.claude/guidelines/go-backend/module-notes/pkg-cmd.md deleted file mode 100644 index d993ca78d1..0000000000 --- a/.claude/guidelines/go-backend/module-notes/pkg-cmd.md +++ /dev/null @@ -1,124 +0,0 @@ -# Probo -- Go Backend -- pkg/cmd (CLI) - -> Module-specific patterns that differ from stack-wide conventions. -> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). - -## Purpose - -Implements the `prb` CLI tool using cobra. All commands communicate with the Probo backend exclusively via GraphQL over HTTPS. No direct database access. - -## Directory Structure (feature-sliced) - -Unlike the flat package pattern used elsewhere in the backend, the CLI uses a feature-slice layout: - -``` -pkg/cmd/ - root/root.go # Registers all top-level resource groups - / - .go # Group command, aggregates verb sub-commands - / - .go # Leaf command, owns RunE logic -``` - -Example: `pkg/cmd/vendor/vendor.go` groups `list/`, `create/`, `view/`, `update/`, `delete/`. - -## Command Construction Pattern - -Every leaf command receives `*cmdutil.Factory` as its sole dependency: - -```go -// See pattern from: contrib/claude/cli.md -func NewCmdCreateVendor(f *cmdutil.Factory) *cobra.Command { - cmd := &cobra.Command{ - Use: "create", - Short: "Create a vendor", - RunE: func(cmd *cobra.Command, args []string) error { - // 1. Load config - cfg, err := f.Config() - if err != nil { return err } - - // 2. Resolve host + token - hc, err := cfg.DefaultHost() - if err != nil { return err } - - // 3. Resolve organization - orgID := cmd.Flag("org").Value.String() - if orgID == "" { orgID = hc.Organization } - - // 4. Create API client - client := api.NewClient(hc.Host, hc.Token, "/api/console/v1/graphql", 0) - - // 5. Execute GraphQL - result, err := client.Do(cmd.Context(), query, variables) - - // 6. Print output - fmt.Fprintln(f.IOStreams.Out, "Vendor created:", id) - return nil - }, - } - return cmd -} -``` - -## GraphQL Queries as Constants - -GraphQL queries are defined as `const` strings at the package level in leaf command files. Response types are unexported structs local to the leaf package: - -```go -// See pattern from: contrib/claude/cli.md -const createVendorMutation = ` - mutation CreateVendor($input: CreateVendorInput!) { - createVendor(input: $input) { - vendor { id name } - } - } -` - -type createVendorResponse struct { - CreateVendor struct { - Vendor struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"vendor"` - } `json:"createVendor"` -} -``` - -## Flag Conventions - -- Flag naming: kebab-case (`--order-by`, `--inherent-likelihood`) -- Standard short flags: `-L` (limit), `-o` (output), `-q` (query), `-y` (yes) -- Organization resolution: `--org` flag first, then `hc.Organization` from config -- Update commands: only include fields where `cmd.Flags().Changed()` is true -- Delete commands: require `--yes` flag or interactive `huh.NewConfirm()` prompt -- List commands: support `--output json|table` (default is table) - -## Output Formatting - -- Table output via `cmdutil.NewTable` (pre-styled lipgloss/table) -- JSON output via `cmdutil.PrintJSON` -- Truncation message goes to stderr, not stdout -- Single confirmation line for create/update/delete - -## Pagination - -List commands use `api.Paginate[T]` with `--limit` / `-L` flag (default 30): - -```go -// See: pkg/cli/api/pagination.go -nodes, totalCount, err := api.Paginate[vendorNode]( - cmd.Context(), - client, - listVendorsQuery, - variables, - func(raw json.RawMessage) (*api.Connection[vendorNode], error) { ... }, -) -``` - -## Interactive Prompts - -When `f.IOStreams.IsInteractive()` is true, commands use `charmbracelet/huh` for prompts. Non-interactive mode (piped input) skips prompts and requires all data via flags. - -## Registration - -New resource group commands must be registered in `pkg/cmd/root/root.go`. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-coredata.md b/.claude/guidelines/go-backend/module-notes/pkg-coredata.md deleted file mode 100644 index 54719906d0..0000000000 --- a/.claude/guidelines/go-backend/module-notes/pkg-coredata.md +++ /dev/null @@ -1,112 +0,0 @@ -# Probo -- Go Backend -- pkg/coredata - -> Module-specific patterns that differ from stack-wide conventions. -> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). - -## Purpose - -All raw SQL queries, entity struct definitions, filters, order fields, enumerations, and database migrations for every domain object. No ORM -- all SQL is hand-written inline using pgx named arguments. This is the **single source of truth** for database interaction. - -## Entity File Pattern - -One file per entity in snake_case (e.g., `asset.go`, `vendor_risk_assessment.go`). Companion files use suffixes: `_filter.go`, `_order_field.go`, `_type.go`, `_state.go`, `_status.go`. - -Every entity struct uses: -- `gid.GID` for primary key -- `db` struct tags for pgx column mapping -- `time.Time` for CreatedAt/UpdatedAt -- Pointer types for nullable columns -- **No TenantID field** (injected by Scoper at query time) - -```go -// See: pkg/coredata/asset.go -type Asset struct { - ID gid.GID `db:"id"` - SnapshotID *gid.GID `db:"snapshot_id"` - Name string `db:"name"` - OwnerID gid.GID `db:"owner_profile_id"` - OrganizationID gid.GID `db:"organization_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} -``` - -## Method Signatures - -| Method | Receiver | Returns | Notes | -|--------|----------|---------|-------| -| `LoadByID` | `*Entity` | `error` | Assigns into receiver via `*e = entity` | -| `LoadAllBy*` | `*Entities` (slice) | `error` | Paginated with `page.Cursor[OrderField]` | -| `CountBy*` | `*Entities` | `(int, error)` | Uses `COUNT(id)` | -| `Insert` | `*Entity` | `error` | Uses `scope.GetTenantID()` for tenant_id | -| `Update` | `*Entity` | `error` | Uses `RETURNING` and reassigns receiver | -| `Delete` | `*Entity` | `error` | Checks `snapshot_id IS NULL` to prevent deleting snapshots | -| `CursorKey` | `*Entity` | `page.CursorKey` | Switch on OrderField; panics on unknown | -| `AuthorizationAttributes` | `*Entity` | `(map[string]string, error)` | Returns org/tenant IDs for ABAC | - -## Row Collection - -```go -// Single row -- See: pkg/coredata/asset.go -asset, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Asset]) - -// Multiple rows -assets, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Asset]) - -// Map pgx.ErrNoRows to sentinel -if errors.Is(err, pgx.ErrNoRows) { - return ErrResourceNotFound -} -``` - -## Filter Pattern (double-pointer three-state logic) - -Filter fields use double pointers for three-state logic: -- `nil` = no filter -- `*nil` = IS NULL -- `*val` = equals - -```go -// See: pkg/coredata/vendor_filter.go -func (f *VendorFilter) SQLArguments() pgx.StrictNamedArgs { - args := pgx.StrictNamedArgs{ - "show_on_trust_center": nil, // always declared - "has_snapshot_filter": false, // always declared - "filter_snapshot_id": nil, // always declared - } - if f.showOnTrustCenter != nil { - args["show_on_trust_center"] = *f.showOnTrustCenter - } - return args -} -``` - -## OrderField Pattern (complete version) - -Newer order fields implement the full set: `Column()`, `IsValid()`, `String()`, `MarshalText()`, `UnmarshalText()`. Follow the `InvitationOrderField` pattern in `pkg/coredata/invitation_order_field.go` for new entities. - -Some older order fields (e.g., `AssetOrderField`) only implement `Column()` and `String()`. This is an inconsistency -- new entities should use the complete pattern. - -## Migration Rules - -- Files in `pkg/coredata/migrations/` named `YYYYMMDDTHHMMSSZ.sql` (UTC timestamps) -- One logical change per file -- No indexes by default (only when justified by observed latency) -- No DEFAULT clauses on new tables -- When adding non-nullable columns to existing tables: use DEFAULT to backfill, then drop DEFAULT in the same migration -- ON DELETE CASCADE for audit log FKs to organizations - -## Entity Type Registry - -`entity_type_reg.go` contains all entity type uint16 constants. Critical rules: -- Never reuse removed type numbers (tombstone with `_ uint16 = N // Removed`) -- Always append new types at the end -- Every new entity must have a case in the `NewEntityFromID` switch - -## No Tests in This Package - -`pkg/coredata` has no unit tests. Coverage comes from e2e tests in `e2e/console/`. This is intentional -- the package is pure data access with no business logic worth unit-testing in isolation. - -## No Logging - -The data access layer does not log. Errors are returned to callers who log at the service layer. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-llm-agent.md b/.claude/guidelines/go-backend/module-notes/pkg-llm-agent.md deleted file mode 100644 index 66b7a6aa4c..0000000000 --- a/.claude/guidelines/go-backend/module-notes/pkg-llm-agent.md +++ /dev/null @@ -1,137 +0,0 @@ -# Probo -- Go Backend -- pkg/llm and pkg/agent (LLM Integration) - -> Module-specific patterns that differ from stack-wide conventions. -> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). - -## Architecture - -The LLM integration is split into three layers: - -1. **pkg/llm** -- Provider-agnostic LLM abstraction (hexagonal pattern, unique in this codebase) -2. **pkg/agent** -- LLM agent orchestration framework (tool dispatch, guardrails, streaming) -3. **pkg/agents** -- Domain-specific agent implementations (changelog, vendor assessment) - -## pkg/llm -- Provider Abstraction - -### Hexagonal architecture (unique in codebase) - -Unlike the flat pattern used elsewhere, `pkg/llm` uses a hexagonal (ports-and-adapters) architecture: - -- **Core types** (root package): `Provider` interface, `ChatCompletionRequest/Response`, `Message`, `Part`, `Tool`, error types -- **Adapters** (sub-packages): `anthropic/`, `openai/`, `bedrock/` each implement `Provider` -- **Instrumented client** (root package): `Client` wraps any `Provider` with logging and OTel tracing - -### Provider interface - -```go -// See: pkg/llm/provider.go -type Provider interface { - ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) - ChatCompletionStream(ctx context.Context, req ChatCompletionRequest) (ChatCompletionStream, error) -} -``` - -### Canonical error types - -All provider adapters map vendor-specific errors to four canonical types: - -| Error type | Meaning | -|-----------|---------| -| `ErrRateLimit` | API rate limit exceeded | -| `ErrContextLength` | Token limit exceeded | -| `ErrContentFilter` | Content policy violation | -| `ErrAuthentication` | Invalid API credentials | - -### OTel GenAI semantic conventions - -LLM calls are traced with GenAI semantic conventions (semconv v1.37.0). Spans named `chat {model}` with attributes like `gen_ai.operation.name`, `gen_ai.usage.input_tokens`, etc. No PII or message content is ever logged -- only model name, token counts, and duration. - -### Streaming -- always close - -Streaming calls must always call `stream.Close()` even on early exit. The OTel span is only finalized by `Next()` returning false or `Close()`. Forgetting `Close()` leaks the span. - -### Provider-specific gotchas - -- **Anthropic** requires `MaxTokens` to be set (returns `ErrContextLength` if nil) -- **Bedrock** does not support `ToolChoiceNone` (tools are silently omitted) -- **Bedrock** silently drops `FilePart` and `ImagePart` in user messages -- **OpenAI** is the only adapter supporting `ResponseFormat` (JSON schema mode) - -## pkg/agent -- Agent Framework - -### Construction via functional options - -Agents are configured declaratively: - -```go -// See: pkg/agent/agent.go -agent := agent.New( - "my-agent", - "You are a helpful assistant.", - llmClient, - agent.WithTools(myTool), - agent.WithLogger(logger), - agent.WithModel("claude-sonnet-4-20250514"), - agent.WithInputGuardrails(promptInjectionGuard), - agent.WithOutputGuardrails(sensitiveDataGuard), -) -``` - -### Default logger discards output - -The agent's default logger writes to `io.Discard`. You must pass `WithLogger(myLogger)` to see any diagnostics. This is different from service packages where loggers are always wired. - -### Tool interface - -Tools implement a two-tier interface: `ToolDescriptor` (name + JSON schema) and `Tool` (extends with `Execute`). `FunctionTool[P]` is a generic constructor that auto-generates JSON schema from the parameter type: - -```go -// See: pkg/agent/tool.go -tool := agent.FunctionTool("search", "Search the web", func(ctx context.Context, params SearchParams) (agent.ToolResult, error) { - // ... -}) -``` - -### Guardrails - -- **Input guardrails** check messages before LLM call (e.g., `PromptInjectionGuardrail`) -- **Output guardrails** check each assistant message (e.g., `SensitiveDataGuardrail`, `SystemPromptLeakGuardrail`) -- `PromptInjectionGuardrail` **fails open** on classifier error (defense-in-depth philosophy) -- `SensitiveDataGuardrail` has broad patterns (`select `, `update `) that may cause false positives - -### Approval / human-in-the-loop - -Tool calls matching `ApprovalConfig` raise `InterruptedError`. Runs must be resumed via `Resume()`, not re-run via `Run()`. The `InterruptedError` carries opaque state that cannot be reconstructed. - -### Streaming events - -Events are sent on a buffered channel (size 64). If the consumer reads too slowly, events are **dropped silently** -- `trySendEvent` does not block. - -## pkg/agents -- Domain Agents - -Thin facades over `pkg/agent`: - -- `GenerateChangelog(ctx, oldContent, newContent)` -- single-turn text diff summary -- `AssessVendor(ctx, websiteURL)` -- structured vendor info via `RunTyped[vendorInfo]` - -Each method creates a new `agent.Agent` per call (stateless, no session). System prompts are unexported `const` strings. - -**Known issue:** The logger passed to `NewAgent` is stored but never forwarded to inner `agent.New` calls -- inner agents default to `io.Discard` logging. - -## Testing Pattern - -LLM and agent tests use mock implementations rather than live API calls: - -```go -// See: pkg/llm/llm_test.go -type mockProvider struct { - chatCompletionFunc func(...) (*llm.ChatCompletionResponse, error) -} - -// See: pkg/agent/agent_test.go -// mockProvider with pre-scripted responses -// mockChatStream for streaming tests -// recordingHook for verifying hook invocations -``` - -No per-provider integration tests exist. The `pkg/agents` package has no tests at all. diff --git a/.claude/guidelines/go-backend/module-notes/pkg-server-api.md b/.claude/guidelines/go-backend/module-notes/pkg-server-api.md deleted file mode 100644 index 9e791c63b5..0000000000 --- a/.claude/guidelines/go-backend/module-notes/pkg-server-api.md +++ /dev/null @@ -1,172 +0,0 @@ -# Probo -- Go Backend -- pkg/server/api (GraphQL, MCP, Protocol Handlers) - -> Module-specific patterns that differ from stack-wide conventions. -> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md). - -## Three API Surfaces - -The server exposes three API surfaces under `/api/`: - -| API | Path | Auth | Schema source | Codegen | -|-----|------|------|--------------|---------| -| Console GraphQL | `/api/console/v1/graphql` | Session cookie or API key | `schema.graphql` | `go generate ./pkg/server/api/console/v1` | -| Connect GraphQL | `/api/connect/v1/graphql` | Session cookie or API key | `schema.graphql` | `go generate ./pkg/server/api/connect/v1` | -| Trust GraphQL | `/trust/{slug}/api/trust/v1/graphql` | Session cookie (optional) | `schema.graphql` | `go generate ./pkg/server/api/trust/v1` | -| MCP | `/mcp/v1` | API key only | `specification.yaml` | `go generate ./pkg/server/api/mcp/v1` | - -Every feature must be exposed through GraphQL + MCP + CLI. See [shared.md -- Three-interface API surface rule](../shared.md#cross-stack-contracts). - -## GraphQL Resolver Pattern (console/v1, connect/v1, trust/v1) - -### Schema-first with gqlgen - -GraphQL APIs use schema-first code generation via gqlgen. The schema file (`schema.graphql`) is hand-written. Running `go generate` produces: - -- `schema/schema.go` -- executable schema (never edit) -- `types/types.go` -- type struct definitions (never edit) -- `v1_resolver.go` -- resolver stubs (hand-edit method bodies only) - -### Resolver method sequence - -Every resolver follows this strict sequence: - -```go -// See: pkg/server/api/console/v1/v1_resolver.go -func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.CreateVendorPayload, error) { - // 1. Authorize - if err := r.authorize(ctx, input.OrganizationID, probo.ActionVendorCreate); err != nil { - return nil, err - } - - // 2. Get tenant service - prb := r.ProboService(ctx, input.OrganizationID.TenantID()) - - // 3. Call service method - vendor, err := prb.Vendors.Create(ctx, &probo.CreateVendorRequest{...}) - - // 4. Handle errors + map to types - if err != nil { - r.logger.ErrorCtx(ctx, "cannot create vendor", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - return &types.CreateVendorPayload{Vendor: types.NewVendor(vendor)}, nil -} -``` - -### Connection types (Relay cursor pagination) - -Each paginated entity has a `*Connection` struct in `types/` with: -- Edges, PageInfo -- Resolver (parent type for TotalCount dispatch) -- ParentID (gid.GID) -- Filter (optional) - -```go -// See: pkg/server/api/console/v1/types/vendor.go -type VendorConnection struct { - Resolver any - ParentID gid.GID - Filter *coredata.VendorFilter -} -``` - -TotalCount resolvers dispatch on `obj.Resolver.(type)` to handle different parent contexts. Adding a new parent requires updating the type switch. - -### Dataloaders (console/v1 only) - -Per-request batch loaders solve N+1 queries for 11 entity types. Injected via HTTP middleware. - -```go -// See: pkg/server/api/console/v1/dataloader/dataloader.go -loaders := dataloader.FromContext(ctx) -org, err := loaders.Organization.Load(ctx, vendor.OrganizationID) -``` - -Always use dataloaders for entities that have them. Direct service calls cause N+1 queries. - -### Custom scalars - -| GraphQL scalar | Go type | Adapter | -|---|---|---| -| `GID` | `gid.GID` | `gqlutils/types/gid` | -| `Datetime` | `time.Time` | stdlib `time.Time` | -| `CursorKey` | `page.CursorKey` | `gqlutils/types/cursorkey` | -| `Duration` | `time.Duration` | stdlib | -| `BigInt` | `int64` | stdlib | -| `EmailAddr` | `mail.Addr` | `gqlutils/types/addr` | - -### Error helpers (gqlutils) - -All resolver errors must use typed gqlutils helpers, never raw `gqlerror.Error` construction: - -| Helper | Extension code | When to use | -|--------|---------------|-------------| -| `Internal(ctx)` | `INTERNAL_SERVER_ERROR` | Unexpected errors (hides details) | -| `NotFoundf(ctx, ...)` | `NOT_FOUND` | Resource not found | -| `Forbidden(ctx, msg)` | `FORBIDDEN` | Permission denied | -| `Invalidf(ctx, ...)` | `INVALID` | Validation failure | -| `Unauthenticatedf(ctx, ...)` | `UNAUTHENTICATED` | Missing auth | -| `AssumptionRequired(ctx)` | `ASSUMPTION_REQUIRED` | Org session not assumed | -| `NDASignatureRequiredf(ctx, ...)` | `NDA_SIGNATURE_REQUIRED` | Trust center NDA required | - -## MCP Resolver Pattern - -### Schema-first with mcpgen - -MCP tools are defined in `specification.yaml` (hand-written YAML). Running `go generate` produces `server/server.go` and `types/types.go`. - -### Resolver differences from GraphQL - -1. **Authorization uses panic**: `r.MustAuthorize(ctx, id, action)` panics on failure. `RecoveryMiddleware` catches and translates to MCP error. -2. **First return is always nil**: `return nil, output, nil` -3. **API key auth only**: No session cookies. -4. **Type helpers in types/**: One file per entity with `New*` conversion functions (similar to GraphQL types/). -5. **Optional fields use Omittable**: `mcpgen/omittable` type for nullable update fields. Unwrap with `UnwrapOmittable` helper. - -```go -// See: pkg/server/api/mcp/v1/schema.resolvers.go -func (r *Resolver) UpdateVendor(ctx context.Context, req mcp.CallToolRequest, input types.UpdateVendorInput) (*mcp.CallToolResult, *types.UpdateVendorOutput, error) { - r.MustAuthorize(ctx, input.ID, probo.ActionVendorUpdate) - // ... - return nil, types.NewUpdateVendorOutput(vendor), nil -} -``` - -## Authentication Middleware Chain - -The middleware chain order is critical (session -> API key -> identity presence): - -```go -// See: pkg/server/api/console/v1/resolver.go -mux.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig)) -mux.Use(authn.NewAPIKeyMiddleware(iamSvc, tokenSecret)) -mux.Use(authn.NewIdentityPresenceMiddleware()) -``` - -- Session and API key are mutually exclusive (checked symmetrically) -- Identity presence must be last (only checks context, does not authenticate) -- Unexpected errors in middleware panic (caught by upstream recovery) - -## Trust Center GraphQL (@nda directive) - -The trust API has a unique `@nda` directive that gates field resolution behind completed electronic signatures: - -```graphql -# See: pkg/server/api/trust/v1/schema.graphql -type Document implements Node @nda { ... } -``` - -The directive handler checks NDA completion at field resolution time. New types with NDA-gated content must have `@nda` applied on the type or each field individually. - -## Connect API (SAML/OIDC/SCIM handlers) - -The connect/v1 module mounts protocol handlers alongside its GraphQL API: - -| Endpoint | Handler | Protocol | -|----------|---------|----------| -| `/graphql` | gqlgen | GraphQL | -| `/saml/2.0/*` | SAMLHandler | SAML 2.0 SP | -| `/oidc/*/*` | OIDCHandler | OIDC/OAuth2 | -| `/scim/2.0/*` | SCIMHandler | SCIM 2.0 | - -SAML and OIDC handlers bypass GraphQL entirely -- they call IAM services directly, set secure cookies, and issue HTTP redirects. diff --git a/.claude/guidelines/go-backend/module-notes/probo.md b/.claude/guidelines/go-backend/module-notes/probo.md new file mode 100644 index 0000000000..b8cf45503a --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/probo.md @@ -0,0 +1,55 @@ +# Probo — Go Backend — pkg/probo + +**Purpose.** Core domain service layer. Implements every business-logic +service for every entity (vendor, control, risk, finding, document, +asset, audit, ...) plus cross-tenant export workers. `Service` is +the root, `Service.WithTenant(tenantID)` returns a `*TenantService`, +and each entity sub-service hangs off `TenantService` as a public +field. + +> See [patterns.md § 1](../patterns.md#1-service--tenantservice--the-domain-service-shape) +> and [§ 2 Request+Validate](../patterns.md#2-request--validate). + +**Key files.** + +- `service.go` — `Service`, `TenantService`, `WithTenant`, root + workers (`ExportJob`, `lockExportJob`). +- `vendor_service.go` — canonical sub-service: full CRUD, + `Request + Validate`, `pg.WithTx` with in-tx `webhook.InsertData`, + double-pointer optional update fields. +- `finding_service.go` — cross-field validation (risk_id required when + status=risk_accepted). +- `document_service.go` — complex sub-service with custom error types + (`ErrSignatureNotCancellable`, `ErrDocumentVersionNotDraft`, ...) and + PDF export / signing flows. +- `actions.go` — all `core:*` action constants. +- `policies.go` — role-keyed policies registered into the IAM + Authorizer at startup. +- `evidence_description_worker.go` — canonical worker (Claim / Process / + RecoverStale, FOR UPDATE SKIP LOCKED). + +**How to extend (a new entity).** + +The 4-file checklist (see [pitfalls.md § 11](../pitfalls.md)): + +1. `pkg/probo/_service.go` — sub-service struct with `svc + *TenantService` field, `Request + Validate`, `pg.WithTx`, scope use. +2. `pkg/probo/service.go` — add `*Service` field to + `TenantService` struct + initialise it in `WithTenant`. +3. `pkg/probo/actions.go` — `Action` constants. +4. `pkg/probo/policies.go` — `Allow` statements per role with + `organizationCondition`. + +Plus the coredata side (entity file + filter + order field + +entity_type_reg). + +**Top pitfalls.** + +- Performing authorization inside a service method. **Don't.** Service + methods are auth-free; resolvers authorize first. +- Constructing a fresh `Scoper` inside a sub-service method. Always use + `s.svc.scope`. Only root-Service workers (e.g. `ExportJob`) build + their own scope from a claimed entity ID. +- Calling `webhook.InsertData` outside the entity-mutation transaction — + partial-state risk. +- Using `*string` instead of `**string` for nullable update fields. diff --git a/.claude/guidelines/go-backend/module-notes/probod.md b/.claude/guidelines/go-backend/module-notes/probod.md new file mode 100644 index 0000000000..567bf37bd3 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/probod.md @@ -0,0 +1,50 @@ +# Probo — Go Backend — pkg/probod + +**Purpose.** Single composition root. Wires every subsystem into the +`probod` daemon: builds all services, runs DB migrations, launches +the API HTTP server, the Trust HTTP/HTTPS server, every worker, every +sender, and orchestrates graceful shutdown. + +> See [patterns.md § 6 Service orchestration](../patterns.md#6-service-orchestration-probod). + +**Key files.** + +- `probod.go` — `Implm` (`unit.Configurable` + `unit.Runnable`), + `New()`, `Run()`, `runApiServer`, `runTrustCenterServer`, every + `runXxxWorker` and `runXxxSender`. +- `llm.go` — `resolveAgentClient`, `buildLLMClient` (per-named-agent + provider wiring with logger/tracer/prometheus). +- `vendor_assessor.go` — opt-in factory: returns + `probo.DisabledVendorAssessor` if no provider is configured. + +**How to extend (a new subsystem).** + +1. Add config fields to the appropriate `Config` substruct (and + propagate to all 11 config files — see + [shared.md § 4](../../shared.md#4-configuration-propagation)). +2. Construct the subsystem inside `Run()` after migrations and before + the goroutine launch block. +3. Launch with the standard pattern: + ```go + subCtx, stopSub := context.WithCancel(context.Background()) // <-- Background, not ctx + wg.Go(func() { + if err := sub.Run(subCtx); err != nil { + cancel(fmt.Errorf("subsystem crashed: %w", err)) + } + }) + ``` +4. Add `stopSub()` to the shutdown sequence (after `<-ctx.Done()`, + before `wg.Wait()`). +5. **Don't forget the stop function** — see + [pitfalls.md § 7](../pitfalls.md). + +**Top pitfalls.** + +- Forgetting the `stopXxx()` call after `<-ctx.Done()` — worker keeps + running after shutdown. +- Adopting `errgroup` at the top level — breaks the independent-lifetime + contract. Only `runTrustCenterServer` uses errgroup, intentionally. +- Closing `pgClient` before `wg.Wait()` — abandoned in-flight queries. + `pgClient.Close()` is the very last call. +- DB migrations are synchronous — failures abort startup. Don't try to + parallelise them. diff --git a/.claude/guidelines/go-backend/module-notes/server-apis.md b/.claude/guidelines/go-backend/module-notes/server-apis.md new file mode 100644 index 0000000000..31359dad2e --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/server-apis.md @@ -0,0 +1,68 @@ +# Probo — Go Backend — pkg/server/api/* (console, trust, connect, mcp, cookiebanner) + +**Purpose.** All HTTP and GraphQL/MCP API surfaces. Five sibling +packages, each with its own router, schema, and resolver layer: + +| Surface | Package | Generator | Mounted at | +| --- | --- | --- | --- | +| Console (authenticated) | `pkg/server/api/console/v1` | gqlgen | `/api/console/v1/graphql` | +| Trust Center (public) | `pkg/server/api/trust/v1` | gqlgen | served by separate Trust HTTP/HTTPS server | +| Connect (sign-up, invite) | `pkg/server/api/connect/v1` | gqlgen | `/api/connect/v1/graphql` | +| MCP (model context protocol) | `pkg/server/api/mcp/v1` | mcpgen | `/api/mcp/v1` | +| Cookie banner (public) | `pkg/server/api/cookiebanner` | none | `/api/cookiebanner/...` | + +Plus the shared error helpers in `pkg/server/gqlutils` (typed error +constructors with extensions.code) and the cross-resolver +`pkg/server/api/authz` (`AuthorizeFunc`). + +> See [patterns.md § 7 GraphQL resolver shape](../patterns.md#7-graphql-resolver-shape) +> and [§ 8 MCP resolver shape](../patterns.md#8-mcp-resolver-shape). + +**Key files.** + +- `console/v1/resolver.go` — `Resolver` struct, `r.authorize`, + `r.ProboService`, chi router (authn middleware, dataloader middleware, + /graphql, connector OAuth2 routes). +- `console/v1/graphql/*.graphql` — schema (`base.graphql` defines + directives + `Query` + empty `Mutation`; one file per entity + uses `extend type Mutation`). +- `console/v1/types/*.go` — hand-written `types.NewVendor(coredata)` etc. + + `OrderBy[T]` generic, Connection/Edge structs with + `Resolver`+`ParentID` fields for totalCount. +- `console/v1/dataloader/dataloader.go` — per-request batch loaders. +- `console/v1/vendor_resolvers.go` — canonical resolver file (one per + schema file by gqlgen `follow-schema` layout). +- `mcp/v1/specification.yaml` — MCP tool catalog (regenerate with + `go generate ./pkg/server/api/mcp/v1`). +- `mcp/v1/types/*.go` — one file per entity, builders + Omittable + helpers. +- `gqlutils/errors.go` — `NotFound`, `Forbidden`, `Conflict`, `Invalid`, + `Unauthenticated`, `Internal`, `Unavailable`, `AlreadyAuthenticated`. + +**How to extend (a new GraphQL field/mutation).** + +1. Edit the `.graphql` schema file (use `extend type Mutation` only; + never `extend type` anything else). +2. Run `go generate ./pkg/server/api/console/v1` (or the relevant + surface). +3. Implement the new resolver method in `_resolvers.go`: + `r.authorize` first line, error switch with mandatory `default:`. +4. Add the matching MCP tool in `specification.yaml`, regenerate, and + implement the body next to the generated stub. Use `MustAuthorize`. +5. Add the matching `prb` CLI verb file under + `pkg/cmd///.go`. +6. Add the n8n action — see [shared.md § 3](../../shared.md#3-the-four-surface-api-rule). +7. Add an e2e test (`e2e/console/_test.go` or `e2e/mcp/...`). + +**Top pitfalls.** + +- Missing `default:` in resolver error switches — leaks internal errors. +- Forgetting `@goModel` on a Connection type — `totalCount` returns 0 + or panics. +- Adding the GraphQL field but skipping MCP/CLI/n8n — frequency-2 + reviewer rule (PR #1132). +- Direct service calls in field resolvers instead of DataLoader — N+1. +- `extend type X` for `X` other than `Mutation` — gqlgen mis-routes the + resolver. +- Surfacing GraphQL fields as non-null (`!`) when the resolver can fail — + use Relay `@required` instead (frequency-4 reviewer rule). diff --git a/.claude/guidelines/go-backend/module-notes/trust.md b/.claude/guidelines/go-backend/module-notes/trust.md new file mode 100644 index 0000000000..5de273e3b5 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/trust.md @@ -0,0 +1,34 @@ +# Probo — Go Backend — pkg/trust + +**Purpose.** Trust-center domain logic — the public-facing compliance +artefact pages served by the Trust HTTP/HTTPS server. Owns the +**visibility model** that gates each artefact (public, NDA-required, +authenticated, ...) and the access-grant + email flow that issues +access to NDA-gated content. + +**Key files.** + +- `pkg/trust/service.go` — `Service`, visibility model, grant flow. +- `pkg/trust/grant.go` — `GrantByIDs` and related grant helpers. +- `pkg/trust/logo.go` — `GenerateLogoURL`. + +**How to extend.** + +- A new visibility level: extend the enum, add a migration in + `pkg/coredata`, update the API mappings (GraphQL trust schema, + cookie-banner public endpoint if applicable). +- A new artefact type: define the entity in `pkg/coredata`, add a + visibility column, surface it via `pkg/server/api/trust/v1`. + +**Top pitfalls.** + +- `pkg/trust/grant.go` line 387 has an inverted `shouldSendEmail` + condition — emails go out when they should not, or the reverse. See + [pitfalls.md § 8](../pitfalls.md). Add an e2e Mailpit assertion when + you touch this file. +- `GenerateLogoURL` swallows errors; trust pages render with a missing + logo. Update the signature to return an error and propagate. See + [pitfalls.md § 9](../pitfalls.md). +- Trust-center HTTP server is a separate subsystem in + `pkg/probod/probod.go` (uses `errgroup` internally) — see + [probod.md](./probod.md). diff --git a/.claude/guidelines/go-backend/module-notes/validator.md b/.claude/guidelines/go-backend/module-notes/validator.md new file mode 100644 index 0000000000..f130b55582 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/validator.md @@ -0,0 +1,40 @@ +# Probo — Go Backend — pkg/validator + +**Purpose.** Fluent in-memory validation framework used by every +mutating service request. No I/O. + +**Key files.** + +- `errors.go` — `Validator`, `ValidationError`, `ValidationErrors` + (named `[]*ValidationError` implementing `error`), `ErrorCode` + string constants (`REQUIRED`, `INVALID_FORMAT`, `OUT_OF_RANGE`, ...). +- Built-in validators: `Required`, `NotEmpty`, `GID(entityTypes...)`, + `SafeText`, `SafeTextNoNewLine`, `MinLen`, `MaxLen`, `Min`, `Max`, + `OneOfSlice`, `URL`, `HTTPSUrl`, `Domain`, `NoHTML`, `PrintableText`, + `NoNewLine`, `After`, `Before`, `RangeDuration`. + +**How to use (canonical).** + +```go +func (r CreateVendorRequest) Validate() error { + v := validator.New() + v.Check(r.Name, "name", validator.Required(), validator.MaxLen(NameMaxLength)) + v.Check(r.Description, "description", validator.MaxLen(DescriptionMaxLength)) + v.Check(r.Email, "email", validator.Required(), validator.Email()) + return v.Error() // nil or ValidationErrors +} +``` + +**How to extend.** Add a new built-in by writing a `func(value any) +*ValidationError` and exposing it as `func XXX() CheckFunc { return ... }`. +Add a new `ErrorCode` constant if the failure category is genuinely +new — otherwise reuse `INVALID_FORMAT` or `CUSTOM`. + +**Top pitfalls.** + +- Re-using a `Validator` across calls. `New()` allocates fresh state; + the Validator is per-call. +- Storing the offending value in `ValidationError.Value` and then + logging the entire `ValidationErrors` payload — that may include PII + (emails, names). Log only `Field` and `Code`; surface `Message` and + `Value` to the user via the API response, not to logs. diff --git a/.claude/guidelines/go-backend/module-notes/webhook-and-slack.md b/.claude/guidelines/go-backend/module-notes/webhook-and-slack.md new file mode 100644 index 0000000000..8a8c752476 --- /dev/null +++ b/.claude/guidelines/go-backend/module-notes/webhook-and-slack.md @@ -0,0 +1,50 @@ +# Probo — Go Backend — pkg/webhook + pkg/slack + +**Purpose.** Outbox-pattern outbound notification system. Domain +mutations write a row inside the same transaction; a background sender +drains the queue and delivers over HTTPS (webhook) or via the Slack +Web API (slack). + +> See [patterns.md § 10 Webhook outbox / payload DTOs](../patterns.md#10-webhook-outbox--payload-dtos). + +**Key files.** + +- `pkg/webhook/data.go` — `InsertData(ctx, tx, scope, orgID, eventType, + dto)` transactional emission API; `Payload` envelope (eventId, + subscriptionId, organizationId, eventType, createdAt, data). +- `pkg/webhook/sender.go` — poll loop (own loop, not `kit/worker`), + HMAC-SHA256 signing (`X-Probo-Webhook-Signature`, + `X-Probo-Webhook-Timestamp`), per-subscription signing-secret cache + (`sync.Map`). +- `pkg/webhook/types/.go` — public DTOs (`NewVendor`, + `NewUser`, `NewObligation`, ...) — **the only types allowed in the + payload `data` field**. +- `pkg/coredata/webhook_subscription.go` — encrypted signing secret + (`EncryptedSigningSecret []byte`, AES-256-GCM). +- `pkg/coredata/webhook_data.go` — outbox row, `LoadNextUnprocessedForUpdate` + (FOR UPDATE SKIP LOCKED). +- `pkg/coredata/webhook_event.go` — per-subscription delivery attempt. +- `pkg/slack/sender.go` — sibling outbox for Slack messages. + +**How to extend (a new event).** + +1. Add an event-type constant to `pkg/coredata/webhook_event_type.go` + (`namespace:action`). +2. Add a DTO in `pkg/webhook/types/.go` if it doesn't exist — + `NewXxx(coredata.Xxx) Xxx` constructor. +3. Call `webhook.InsertData(ctx, tx, scope, orgID, "namespace:action", + webhooktypes.NewXxx(entity))` **inside** the entity-mutation + transaction. +4. Add an e2e test that exercises the GraphQL mutation and asserts the + webhook payload (use the Mailpit pattern but for webhooks — capture + the delivery via a fake HTTP server). + +**Top pitfalls.** + +- Passing a raw `coredata` struct as the DTO. Compiles, runs, leaks + internals. Frequency-2 reviewer rule. See + [pitfalls.md § 14](../pitfalls.md). +- Calling `webhook.InsertData` outside the transaction. +- Sender is **not** `kit/worker`-based — don't try to use stale-recovery + primitives from there. The Sender's own loop handles retries via + the `WebhookEvent` status column. diff --git a/.claude/guidelines/go-backend/patterns.md b/.claude/guidelines/go-backend/patterns.md index b54fd3977d..3bea67ff08 100644 --- a/.claude/guidelines/go-backend/patterns.md +++ b/.claude/guidelines/go-backend/patterns.md @@ -1,282 +1,659 @@ -# Probo -- Go Backend -- Patterns +# Probo — Go Backend — Patterns -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) +> Cross-stack principles (PII-free logging, SSRF defaults, error wrapping +> rule, tenant isolation, GID layout) live in [shared.md](../shared.md). +> This file documents Go-specific patterns universal to all `pkg/*` code, +> with module-specific deviations called out inline. -## Code Organization +--- -### Two-level service tree (universal) +## 1. Service / TenantService — the domain-service shape -Every domain service follows a two-level pattern: a global `Service` singleton and a tenant-scoped `TenantService` created via `Service.WithTenant(tenantID)`. - -The global `Service` holds infrastructure dependencies (pg.Client, S3, LLM, logger). `TenantService` bundles a `coredata.Scoper` for tenant isolation and exposes all domain sub-services as public fields. +**Universal pattern** (all of `pkg/probo`; mirrored by `pkg/iam`, +`pkg/trust`, `pkg/cookiebanner`, `pkg/esign`, `pkg/connector` for their +respective domains). ``` -// See: pkg/probo/service.go -Service (global) -> WithTenant(tenantID) -> TenantService - .Vendors VendorService - .Documents DocumentService - .Frameworks FrameworkService - ... (~40 sub-services) +NewService(ctx, encryptionKey, pgClient, s3Client, ...) → *Service +Service.WithTenant(tenantID) → *TenantService +TenantService.Vendors / .Controls / .Risks / ... → *FooService +FooService.Operation(ctx, req) → entity, error ``` -Sub-services are thin structs with a single `svc *TenantService` field. Each corresponds to one file named `_service.go`. +- `Service` (root, tenant-agnostic) holds infrastructure: `*pg.Client`, + `*log.Logger`, S3 client, `*llm.Client`, file manager, esign, connectors, + cipher key. It also owns cross-tenant workers (e.g. `Service.ExportJob`). +- `TenantService` carries the `coredata.Scoper` (built from `tenantID`) + and **exposes every entity sub-service as a public field**. +- Each sub-service (`VendorService`, `ControlService`, `FindingService`) + has a single `svc *TenantService` field. Methods read `s.svc.scope`, + `s.svc.pg`, `s.svc.logger` — they never construct a new `Scoper`. +- Service methods are **authorization-free**. IAM checks happen in the + GraphQL/MCP resolver before the service is called + (see [§ 4 Authorization](#4-authorization)). Adding `authorize()` calls + inside a `pkg/probo` method is incorrect. -This pattern is used universally across `pkg/probo`, `pkg/iam`, and `pkg/trust`. +> See `pkg/probo/service.go` and `pkg/probo/vendor_service.go`. -### Request struct + Validate() (universal) +### Workers attached to the root Service -Every mutating service method takes a typed `*Request` struct with a `Validate() error` method that uses the fluent `pkg/validator` API. Validation is always the first call inside the service method body. +Cross-tenant workers (e.g. `ExportJob`) live on `*Service`, **not** on +`TenantService`, because they need to operate across all tenants. They +must construct their own `Scoper` from the claimed entity: ```go -// See: pkg/probo/vendor_service.go -func (r *CreateVendorRequest) Validate() error { - v := validator.New() - v.Check(r.Name, "name", validator.SafeText(NameMaxLength)) - v.Check(r.WebsiteURL, "websiteURL", validator.HTTPSUrl()) - return v.Error() -} +// pkg/probo/service.go — pattern shown in lockExportJob +scope := coredata.NewScopeFromObjectID(exportJob.ID) ``` -### All SQL in pkg/coredata only (universal) +--- -This is the most fundamental architectural constraint. See [shared.md -- Architecture Decisions](../shared.md#cross-stack-contracts) for the project-wide rule. +## 2. Request + Validate + +**Universal pattern** for every mutating service method. + +```go +// pkg/probo/vendor_service.go (canonical) +type CreateVendorRequest struct { + Name string + Description *string + // ... +} -All raw SQL lives exclusively in `pkg/coredata`. Service packages (`pkg/probo`, `pkg/iam`, `pkg/trust`) call coredata model methods (LoadByID, Insert, Update, Delete) inside `pg.WithConn` or `pg.WithTx` closures. No other package may contain SQL queries. +func (r CreateVendorRequest) Validate() error { + v := validator.New() + v.Check(r.Name, "name", validator.Required(), validator.MaxLen(NameMaxLength)) + v.Check(r.Description, "description", validator.MaxLen(DescriptionMaxLength)) + return v.Error() +} -## Error Handling +func (s *VendorService) Create(ctx context.Context, req CreateVendorRequest) (*coredata.Vendor, error) { + if err := req.Validate(); err != nil { + return nil, err + } + // ... pg.WithTx, coredata.Insert, webhook.InsertData ... +} +``` -### Error wrapping convention (universal, enforced in code review) +- `Validate()` is **the first line** of every mutating method. It returns + a `validator.ValidationErrors` (a typed slice implementing `error`), + which the GraphQL/MCP/CLI layer detects with `errors.As` and surfaces + as field-level form errors — see [shared.md § 11](../shared.md#11-error-handling-principles). +- `validator.New()` is allocated **per call** (the validator is a stateful + accumulator, not a long-lived service). +- The framework auto-dereferences pointers, so `v.Check(r.Description, …)` + works whether `Description` is `*string`, `**string`, or a plain string. +- Cross-field rules (e.g. *"`risk_id` is required when status = + risk_accepted"*) live inside `Validate()` — see + `pkg/probo/finding_service.go`. -All errors are wrapped with `fmt.Errorf` using lowercase messages starting with `cannot`: +### Update requests use double pointers ```go -// See: pkg/coredata/custom_domain.go -return nil, fmt.Errorf("cannot load custom domain: %w", err) +type UpdateVendorRequest struct { + ID gid.GID + Name *string // single pointer: nil = no change, non-nil = set + Description **string // double pointer: outer nil = no change, outer non-nil + inner nil = set NULL +} ``` -Using "failed to" or other prefixes is an **approval blocker** in code review. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). +Single-pointer optional fields conflate "not provided" with "set to null". +For nullable columns where the API must support both, use `**T`. The +validator and the `coredata` Update method both understand this. -### Sentinel errors (universal -- pkg/coredata) +--- -Three sentinel errors defined in `pkg/coredata/errors.go`: +## 3. Worker pattern (poll-based + FOR UPDATE SKIP LOCKED) -| Sentinel | Mapped from | Usage | -|----------|------------|-------| -| `ErrResourceNotFound` | `pgx.ErrNoRows` | Resource lookup failures | -| `ErrResourceAlreadyExists` | PostgreSQL error code 23505 (unique violation) | Insert conflicts | -| `ErrResourceInUse` | PostgreSQL FK constraint violation | Deletion blocked by references | +**Universal pattern** for every background job: mailer, slack, esign, +evidence describer, export, webhook delivery, custom-domain renewer. -Service packages map these to domain-specific typed errors: +> Source: [`contrib/claude/go-worker.md`](../../../contrib/claude/go-worker.md). +> Library: `go.gearno.de/kit/worker`. ```go -// See: pkg/iam/auth_service.go -if errors.Is(err, coredata.ErrResourceNotFound) { - return nil, NewErrIdentityNotFound(email) +// pkg/probo/evidence_description_worker.go (canonical) +type evidenceDescriptionHandler struct { + pg *pg.Client + files *filemanager.Service + describer *evidencedescriber.Describer + logger *log.Logger + staleAfter time.Duration } -``` -### Custom typed error structs (universal -- service packages) +// 1. Claim — atomic state transition PENDING → PROCESSING +func (h *evidenceDescriptionHandler) Claim(ctx context.Context) (*coredata.Evidence, error) { + var ev *coredata.Evidence + err := h.pg.WithTx(ctx, func(tx pg.Tx) error { + e, err := coredata.LoadNextPendingDescriptionForUpdateSkipLocked(ctx, tx, coredata.NewNoScope()) + if errors.Is(err, coredata.ErrResourceNotFound) { + return worker.ErrNoTask // <-- mandatory translation + } + if err != nil { + return fmt.Errorf("cannot claim evidence: %w", err) + } + // ... mark PROCESSING and update ... + ev = e + return nil + }) + return ev, err +} -Domain error types are constructed via `New*Error()` factory functions and collected in `errors.go` per package. Each type implements `Error() string` and optionally `Unwrap() error`. +// 2. Process — long work outside any transaction +func (h *evidenceDescriptionHandler) Process(ctx context.Context, ev *coredata.Evidence) error { + // ... S3 fetch, LLM call, then a fresh tx to commit COMPLETED/FAILED ... +} -| Package | Error file | Example types | -|---------|-----------|--------------| -| `pkg/iam` | `errors.go` | ErrInvalidCredentials, ErrSessionExpired, ErrAssumptionRequired | -| `pkg/iam/saml` | `errors.go` | ErrSAMLConfigurationNotFound, ErrReplayAttackDetected | -| `pkg/iam/oidc` | `errors.go` | ErrProviderNotEnabled, ErrPersonalAccountNotAllowed | -| `pkg/trust` | `errors.go` | ErrPageNotFound, ErrDocumentNotVisible | +// 3. RecoverStale — resets PROCESSING rows older than staleAfter (default 5 min) +func (h *evidenceDescriptionHandler) RecoverStale(ctx context.Context) error { ... } +``` -Callers use `errors.Is` / `errors.As` for type checks -- never string comparison. +### Worker rules (universal) -### GraphQL error mapping (universal -- resolver packages) +1. **Claim must translate `coredata.ErrResourceNotFound` into + `worker.ErrNoTask`.** The kit's loop interprets `ErrNoTask` as + "sleep until next poll"; any other error triggers backoff. +2. **Claim runs inside `pg.WithTx`**, uses `LoadNext*ForUpdateSkipLocked`, + and updates the row's status to a sentinel value (`PROCESSING`). +3. **Process runs outside any transaction** so long external calls + (S3, LLM, HTTP) do not hold DB locks. Commits use a fresh transaction. +4. **`RecoverStale` is mandatory** — implement `worker.StaleRecoverer` + to release rows stuck in `PROCESSING` after `staleAfter` (default 5 + minutes). Without it, a process crash leaves rows stuck forever. +5. **Failure path**: on any error in Process, transition status to + `FAILED` (or equivalent) and `log.Error(err)` *before* returning the + wrapped error to the kit. -GraphQL resolvers use `gqlutils` helper functions from `pkg/server/gqlutils/errors.go`: +### Webhook sender deviates intentionally -| Helper | When to use | -|--------|------------| -| `gqlutils.Internal(ctx)` | Unexpected errors (hides details from client, logs internally) | -| `gqlutils.NotFoundf(ctx, msg, ...)` | Resource not found | -| `gqlutils.Forbidden(ctx, msg)` | Authorization failure | -| `gqlutils.Unauthenticatedf(ctx, msg, ...)` | Missing/invalid authentication | -| `gqlutils.Invalidf(ctx, msg, ...)` | Validation failure | -| `gqlutils.AssumptionRequired(ctx)` | Organization session not assumed | +`pkg/webhook/sender.go` runs its own poll loop (not `kit/worker`) +because it fans out one `WebhookData` to N `WebhookEvent` rows; the +generic worker pattern is single-task. Use `kit/worker` for everything +else. -Always log unexpected errors before returning `gqlutils.Internal`: +--- -```go -// See: pkg/server/api/console/v1/v1_resolver.go -r.logger.ErrorCtx(ctx, "cannot load vendor", log.Error(err)) -return nil, gqlutils.Internal(ctx) -``` +## 4. Authorization -### MCP panic-based authorization (module-specific -- pkg/server/api/mcp/v1) +**Universal pattern** at every API surface. -MCP resolvers use `MustAuthorize()` which deliberately panics on authorization failure. A `RecoveryMiddleware` catches the panic and translates `iam.ErrInsufficientPermissions` to a permission-denied MCP error. This differs from GraphQL resolvers which return errors. +> Source: [`contrib/claude/authorization.md`](../../../contrib/claude/authorization.md). ```go -// See: pkg/server/api/mcp/v1/schema.resolvers.go -r.MustAuthorize(ctx, input.ID, probo.ActionVendorUpdate) +// pkg/server/api/console/v1/vendor_resolvers.go +func (r *queryResolver) Vendor(ctx context.Context, id gid.GID) (*types.Vendor, error) { + if err := r.authorize(ctx, id, probo.ActionVendorRead); err != nil { + return nil, err // gqlutils.Forbidden / gqlutils.Unauthenticated + } + // ... fetch and map ... +} ``` -## Data Access - -### Scoper pattern for tenant isolation (universal -- pkg/coredata) - -Every query is tenant-scoped via the `Scoper` interface. Entity structs do NOT have a `TenantID` field -- `tenant_id` is injected at query time. +- **Every** GraphQL field resolver and **every** MCP tool body starts with + an authorize call as the first line, before any data access. +- Action constants (`probo.ActionVendorRead`, `probo.ActionVendorUpdate`, + ...) live in `pkg/probo/actions.go` for the core service, and + `pkg/iam/iam_actions.go` for IAM operations. Format: + `service:resource:operation` (e.g. `core:vendor:read`, + `iam:organization:update`). +- Policies are **Go code, not DB rows**. They are assembled in + `pkg/probo/policies.go` (and `pkg/iam/iam_policy_set.go`) using the + fluent builder from `pkg/iam/policy`: + + ```go + policy.Allow(probo.ActionVendorRead). + WithResources(policy.NewResourcePattern("...")). + When(organizationCondition()) // attribute-based scoping + ``` +- The `organizationCondition` matches `resource.organization_id` against + `principal.organization_id` — this is **mandatory** on every product + policy. Without it, a policy leaks across organizations. +- Resource entities implement `iam.AuthorizationAttributer`: + + ```go + func (v *Vendor) AuthorizationAttributes() map[string]string { + return map[string]string{"organization_id": v.OrganizationID.String()} + } + ``` + +### Documentation drift + +The `contrib/claude/authorization.md` doc references `policy.In(...)` and +`policy.NotIn(...)` builder helpers. **They do not exist in the source** +(`pkg/iam/policy/statement.go`). Use the `Condition` struct directly with +`ConditionOperator` `In` / `NotIn`. See [pitfalls.md](./pitfalls.md). + +--- + +## 5. SQL composition (coredata) + +**Universal pattern** — and a hard project rule: +**all SQL lives in `pkg/coredata`, one file per entity. Reviewers reject +inline SQL anywhere else** (see [shared.md § 13](../shared.md#13-code-review-enforced-standards)). -Two implementations: -- `Scope` -- adds `tenant_id = @tenant_id` WHERE clause (normal queries) -- `NoScope` -- returns `TRUE` (cross-tenant admin queries) +```go +// pkg/coredata/cookie_banner.go (canonical) +const queryTemplate = ` +SELECT %s +FROM cookie_banners cb +WHERE %s + AND %s -- scope.SQLFragment() + AND %s -- filter.SQLFragment() +ORDER BY %s +LIMIT %s +` + +func (cb *CookieBanner) LoadByOrganizationID( + ctx context.Context, conn pg.Querier, scope Scoper, + organizationID gid.GID, filter *CookieBannerFilter, cursor *page.Cursor[CookieBannerOrderField], +) (*page.Page[CookieBanner, CookieBannerOrderField], error) { + q := fmt.Sprintf(queryTemplate, columns(), filter.SQLFragment(), scope.SQLFragment(), cursor.SQLFragment(), ...) + args := pgx.StrictNamedArgs{"organization_id": organizationID} + maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) + maps.Copy(args, cursor.SQLArguments()) + rows, err := conn.Query(ctx, q, args) + // ... pgx.CollectRows + RowToStructByName ... +} +``` -**Warning:** `NoScope.GetTenantID()` panics by design. Never call it. +### SQL composition rules (universal) + +1. **Templates are static, with `%s` placeholders for fragments only.** + No runtime conditional string building. `fmt.Sprintf` injects scope, + filter, cursor, and column-list fragments at call time. +2. **`pgx.StrictNamedArgs` only** — never `pgx.NamedArgs`. StrictNamedArgs + panics on unknown keys at runtime, catching parameter typos. +3. **`maps.Copy(args, scope.SQLArguments())`** — every Scoper / + Filter / Cursor declares all of its argument keys (set to `nil` when + inactive); a missing key crashes StrictNamedArgs. See + `pkg/coredata/cookie_banner_filter.go` for the convention. +4. **Insert uses `pg.Tx`; Read uses `pg.Querier`.** `pg.Querier` is + satisfied by both a connection and a transaction; `pg.Tx` is required + for writes (and `FOR UPDATE` queries). +5. **Tenant ID at insert time only**: `tenant_id := scope.GetTenantID()` + — never store it on the Go struct (sole exception: `Organization`, + which *is* the tenant). +6. **Never hardcode enum values in SQL.** Use Go constants as named + parameters (`@filter_state`, `@status`). Drift: `pkg/coredata/agent_run.go:472` + hardcodes `'PENDING'` — fix opportunistically. +7. **Update with `Exec` + `RowsAffected` check** by default (see + `cookie_banner.go`). `RETURNING` is reserved for entities that + genuinely need the refreshed state (`asset.go`, `agent_run.go`); + don't introduce new ones casually. +8. **`FOR UPDATE SKIP LOCKED`** queries are named + `LoadNextXxxForUpdateSkipLocked`, require `pg.Tx`, and are the + foundation of every worker queue. + +### Migrations + +- Files: `pkg/coredata/migrations/YYYYMMDDTHHMMSSZ.sql` — **UTC date + + random 6-digit time portion** to avoid collisions when multiple + developers branch off `main` simultaneously (per user memory). +- Embedded via `embed.FS` from `pkg/coredata/migrations.go`. +- One logical change per file. **No speculative indexes** — only add + indexes that solve an observed query latency problem in production. +- Sensitive columns are `BYTEA` and follow the three strategies + documented in [shared.md § 12](../shared.md#12-security-baseline-cross-stack): + SHA-256 hash for tokens (`Hashed*`), PBKDF2 for passwords + (`HashedPassword`), AES-256-GCM for decryptable secrets (`Encrypted*`). + +### Entity-type registry + +Adding a new entity requires updating `pkg/coredata/entity_type_reg.go`: +add the next sequential `uint16` constant, add a `case` in +`NewEntityFromID`, and **never reuse a removed number** — leave a `_` +placeholder with a comment explaining the gap. + +--- + +## 6. Service orchestration (probod) + +**Universal pattern** for the composition root. + +> Source: [`contrib/claude/go-service.md`](../../../contrib/claude/go-service.md). ```go -// See: pkg/coredata/asset.go -q := fmt.Sprintf(q, scope.SQLFragment()) -args := pgx.StrictNamedArgs{"asset_id": assetID} -maps.Copy(args, scope.SQLArguments()) -``` +// pkg/probod/probod.go (simplified) +func (impl *Implm) Run(ctx context.Context, l *log.Logger, m prometheus.Registerer, tp trace.TracerProvider) error { + ctx, cancel := context.WithCancelCause(ctx) + defer cancel(nil) + + // 1. Synchronous startup — DB migrations BEFORE any goroutine. + if err := migrator.NewMigrator(pgClient, coredata.Migrations, ...).Run(ctx, "migrations"); err != nil { + return fmt.Errorf("cannot run migrations: %w", err) + } -### pgx.StrictNamedArgs (universal, enforced in code review) + // 2. Build all services (positional injection). + // ... + + // 3. Launch each subsystem with its own background-derived context. + var wg sync.WaitGroup + apiServerCtx, stopApiServer := context.WithCancel(context.Background()) // <-- context.Background, not ctx + wg.Go(func() { + if err := apiServer.Run(apiServerCtx); err != nil { + cancel(fmt.Errorf("api server crashed: %w", err)) + } + }) + // ... mailerCtx, slackSenderCtx, exportWorkerCtx, etc. ... + + // 4. Block on shutdown. + <-ctx.Done() + + // 5. Stop in reverse order (or whatever the dependency chain dictates). + stopApiServer() + // ... stopMailer(), stopSlackSender(), ... + + wg.Wait() + pgClient.Close() // last — guarantees no in-flight queries are abandoned + return context.Cause(ctx) +} +``` -All queries must use `pgx.StrictNamedArgs`, not `pgx.NamedArgs`. StrictNamedArgs rejects any key present in the SQL that is not in the args map at runtime, preventing silent bugs from unset parameters. Using `pgx.NamedArgs` is an **approval blocker**. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). +### Orchestration rules (universal) -### Static SQL fragments (universal, enforced in code review) +1. **Every child context is derived from `context.Background()`, + not the parent `ctx`.** Each subsystem gets an independent lifetime; + the only shutdown signal it receives is its explicit `stopX()` call + after `<-ctx.Done()`. **If you forget the stop function, the worker + runs forever after shutdown begins.** +2. **Crash propagation**: every `wg.Go` block ends with + `cancel(fmt.Errorf(" crashed: %w", err))`. The main + goroutine returns `context.Cause(ctx)` so the wrapped crash error + surfaces at process exit. +3. **Migrations are synchronous and blocking.** No retry, no rollback — + fix the migration and restart. +4. **`pgClient.Close()` is the very last call**, after `wg.Wait()`. +5. `runTrustCenterServer` is the **only** subsystem that uses + `errgroup.WithContext` (for the cert renewer + provisioner + HTTP + + HTTPS quartet that must die together). Do not adopt errgroup at the + top level. +6. `var _ unit.Configurable = (*Implm)(nil)` compile-time assertions are + used to lock in interface satisfaction without runtime cost. -`SQLFragment()` must return a static SQL string -- no conditional string building. This ensures the prepared statement shape is always the same. Use `CASE WHEN` in SQL to handle optional filters. +--- -Conditional string building in `SQLFragment()` is an **approval blocker**. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). +## 7. GraphQL resolver shape -### Argument merging with maps.Copy (universal) +**Universal pattern** across `pkg/server/api/{console,trust,connect}/v1`. -Always use `maps.Copy` to combine args from scope, filter, and cursor. Never manually merge StrictNamedArgs maps. +> Source: [`contrib/claude/graphql.md`](../../../contrib/claude/graphql.md). +> Generator: `gqlgen` with `layout: follow-schema`. ```go -// See: pkg/coredata/asset.go -args := pgx.StrictNamedArgs{"asset_id": assetID} -maps.Copy(args, scope.SQLArguments()) -maps.Copy(args, filter.SQLArguments()) -maps.Copy(args, cursor.SQLArguments()) +// pkg/server/api/console/v1/vendor_resolvers.go (canonical) +func (r *mutationResolver) UpdateVendor(ctx context.Context, input UpdateVendorInput) (*types.UpdateVendorPayload, error) { + if err := r.authorize(ctx, input.ID, probo.ActionVendorUpdate); err != nil { + return nil, err + } + tenantID := input.ID.TenantID() + vendor, err := r.ProboService(ctx, tenantID).Vendors.Update(ctx, probo.UpdateVendorRequest{ + ID: input.ID, + Name: gqlutils.UnwrapOmittable(input.Name), + // ... + }) + if err != nil { + switch { + case errors.Is(err, coredata.ErrResourceNotFound): + return nil, gqlutils.NotFound(err) + case errors.Is(err, coredata.ErrResourceAlreadyExists): + return nil, gqlutils.Conflict(err) + case errors.As(err, &validator.ValidationErrors{}): + return nil, gqlutils.InvalidValidationErrors(err) + default: + r.logger.ErrorCtx(ctx, "cannot update vendor", log.Error(err)) + return nil, gqlutils.Internal(ctx) // <-- mandatory default + } + } + return &types.UpdateVendorPayload{Vendor: types.NewVendor(vendor)}, nil +} ``` -### Transaction handling (universal) - -- `pg.WithTx` for any operation involving multiple writes -- `pg.WithConn` for reads and single-step writes -- Webhook insertion always happens inside the same transaction as the mutating operation +### Resolver rules (universal) + +1. **Authorization first line, always.** +2. **Error switch must include a `default:`** that logs server-side and + returns `gqlutils.Internal(ctx)`. Forgetting `default:` leaks SQL, + stack info, etc. to the wire — see [shared.md § 3](../shared.md#3-the-four-surface-api-rule). +3. **`extend type Mutation` only.** Never `extend type Vendor`, + `extend type Query`, etc. — gqlgen's follow-schema layout puts the + resolver in the wrong file otherwise. +4. **DataLoader for any related-entity field traversal.** Use + `dataloader.FromContext(ctx).Vendor.Load(ctx, id)` inside field + resolvers. Direct service calls cause N+1. +5. **Connection types need `@goModel`** pointing to a hand-written + struct in `types/` so `totalCount` resolver dispatch can find the + `Resolver` and `ParentID` fields. Without it, code compiles but + `totalCount` returns 0 and the dispatcher panics. +6. **Use `types.NewXxx(coredata)`** to map between layers. Never expose + coredata structs as GraphQL types. +7. **Cursor pagination via `types.NewCursor` + `types.NewXxxConnection`**. +8. **GraphQL fields whose resolvers can fail must NOT be non-null (`!`).** + Use Relay `@required` on the client side. Frequency-4 reviewer rule — + see [shared.md § 13](../shared.md#13-code-review-enforced-standards). +9. **`@goField(omittable: true)`** on nullable update inputs; unwrap with + `gqlutils.UnwrapOmittable(input.Field)` to convert `graphql.Omittable[T]` + to `*T` for the service layer. + +--- + +## 8. MCP resolver shape + +**Universal pattern** in `pkg/server/api/mcp/v1`. + +> Source: [`contrib/claude/mcp.md`](../../../contrib/claude/mcp.md). +> Generator: `mcpgen` driven from `specification.yaml`. + +- Tool declarations live in `pkg/server/api/mcp/v1/specification.yaml`; + regenerate with `go generate ./pkg/server/api/mcp/v1`. +- The hand-written tool body sits next to the generated stub. +- Use **`MustAuthorize(ctx, id, action)`** (panicking variant) — the MCP + framework recovers panics and renders an internal error. This is the + **only place** in the codebase that uses panicking authorization. +- **Panic on unexpected** is the convention for state assertions (e.g. + switch defaults that should be unreachable). Errors that the client + can act on still go through `jsonutil.RenderInternalServerError(w)`. +- Type helpers live in `pkg/server/api/mcp/v1/types/*.go` — one file per + entity, each defining `NewXxx(coredata.Xxx)` builders. +- **`Omittable[T]`** distinguishes "field absent" from "field set to + null" in update tool inputs. + +--- + +## 9. CLI command shape + +**Universal pattern** for every leaf command under `pkg/cmd///`. + +> Source: [`contrib/claude/cli.md`](../../../contrib/claude/cli.md). ```go -// See: pkg/probo/vendor_service.go -return s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { - if err := vendor.Insert(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert vendor: %w", err) +// pkg/cmd/risk/list/list.go (canonical) +const listQuery = `query ListRisks($orgId: ID!, $first: Int, $after: CursorKey) { ... }` + +type listResponse struct { /* ... */ } + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var opts listOptions + cmd := &cobra.Command{ + Use: "list", + Short: "List risks", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { return err } + host, hc, err := cfg.DefaultHost() + if err != nil { return err } + client := api.NewClient(host, hc.Token, hc.TokenEndpoint, opts.timeout, api.TokenRefreshOption(cfg.Save)) + results, err := api.Paginate[Risk](ctx, client, listQuery, vars, opts.limit, extractConn) + // ... render to f.IOStreams.Out via cmdutil.NewTable or JSON ... + }, } - return webhook.InsertData(ctx, conn, s.svc.scope, webhooktypes.EventVendorCreated, vendor) -}) + return cmd +} ``` -### Cursor-based pagination (universal) +### CLI rules (universal) -All pagination uses keyset cursor pagination (not OFFSET). Cursors encode as `base64url(JSON)` with `[entity_global_id, sort_field_value]`. Default page size is 25. See `pkg/page` for the shared primitives. +1. **One file per verb**, named after the verb: `list.go`, `create.go`, + `view.go`, `update.go`, `delete.go`. Group commands wire the verbs; + the root wires the groups. +2. **`cmdutil.Factory` is the only DI seam** — every leaf takes + `f *cmdutil.Factory` and reads `IOStreams`, `Version`, `Config()` + from it. +3. **GraphQL string is a `const` in the file**, plus an unexported + `*Response` struct describing the expected payload shape. Inline, + not generated. +4. **Pagination via `api.Paginate[T]`** — generic helper that walks + `edges/pageInfo/totalCount`. +5. **Interactive prompts via `huh`** (charmbracelet), but every `huh` + call must be gated by `f.IOStreams.IsInteractive()` so non-TTY runs + (CI, scripts, `--no-interactive`) fall back to flag-only mode. +6. **Output: tables to `Out`, truncation/info to `ErrOut`** so piping + to another command stays clean. -### No speculative indexes (universal, enforced in code review) +--- -No indexes added by default in migrations. Only add when justified by observed query latency. Unique/constraint indexes are exempt. This was explicitly called out in review (PR #917). +## 10. Webhook outbox / payload DTOs -## Authorization +**Universal pattern** for outgoing events. -### ABAC policy system (universal) +> Module: `pkg/webhook` + `pkg/webhook/types`. -The IAM system uses an AWS-like evaluation model: **explicit deny > explicit allow > implicit deny**. Action strings follow the format `service:resource:operation` (e.g., `core:vendor:create`). - -Five built-in roles: OWNER, ADMIN, VIEWER, AUDITOR, EMPLOYEE. Policies are defined in `pkg/probo/policies.go` and `pkg/iam/iam_policies.go`. +```go +// pkg/probo/vendor_service.go (inside pg.WithTx) +err := webhook.InsertData(ctx, tx, scope, orgID, "vendor:created", webhooktypes.NewVendor(v)) +``` -The authorization call flow: -1. Resolver calls `r.authorize(ctx, resourceID, actionString)` -2. `AuthorizeFunc` (from `pkg/server/api/authz`) extracts identity/session from context -3. Delegates to `iam.Authorizer.Authorize` -4. Authorizer resolves membership/role, evaluates policies, records audit log on Allow +Rules: -### Resource-state access control belongs in ABAC, not UI (universal, enforced in code review) +1. **`webhook.InsertData` must run inside the same `pg.WithTx` as the + entity mutation.** Outside the transaction, you can lose either the + entity or the event. +2. **Payloads must be `pkg/webhook/types.NewXxx(coredata)` DTOs — never + raw coredata structs.** Frequency-2 reviewer rule (PR #720): *"We + can't use coredata object as payload for webhook. We must consider + webhook payload as public API."* There is no compile-time guard; + reviewers spot it. See [pitfalls.md](./pitfalls.md). +3. The Sender goroutine handles HMAC-SHA256 signing + (`X-Probo-Webhook-Signature`, `X-Probo-Webhook-Timestamp`), + per-subscription signing-secret cache, and the FAILED/SUCCEEDED + record on every attempt. -When a resource's state (e.g., archived) affects allowed operations, enforcement must happen in the ABAC policy system (`pkg/probo/policies.go`), not in frontend visibility toggles. Prefer Allow rules on active states over Deny rules on archived states. This was debated in PR #855 and is now an established convention. +The same outbox shape is used by `pkg/slack` (writes +`slack_messages` rows in-tx, drained by a Sender) and `pkg/mailer` +(writes `emails` rows in-tx, drained by the mailer worker). -### AuthorizationAttributer interface (universal -- pkg/coredata) +--- -Resources that need authorization must implement `AuthorizationAttributes(ctx, conn) (map[string]string, error)` in their coredata entity file. This returns attributes (typically `organization_id`) used by ABAC condition evaluation. +## 11. Code generation -## Dependency Injection +**Universal**: codegen is driven by `make generate` / `go generate`. -### Constructor injection (universal) +| Generated artefact | Trigger | Generator | +| --- | --- | --- | +| GraphQL schema/exec/types in each `pkg/server/api/{console,connect,trust}/v1` | Edit `*.graphql`, run `go generate .//v1` | `gqlgen` | +| MCP resolvers in `pkg/server/api/mcp/v1` | Edit `specification.yaml`, run `go generate ./pkg/server/api/mcp/v1` | `mcpgen` | +| `pkg/llm/registry_gen.go` | Run `go generate ./pkg/llm` | `internal/cmd/genmodels` (sources OpenRouter capability data) | -No DI framework. All dependencies are passed as explicit constructor parameters. The pattern varies by layer: +**Never hand-edit a `*_gen.go` file.** Re-run the generator. Generated +files are checked in (so the build does not depend on network access). -- **Service layer**: `NewService(ctx, pgClient, s3Client, config, ...)` with a flat `Config` struct -- **Sub-modules**: sub-services receive `*TenantService` as their single field -- **Background workers**: functional options (`With*` functions) for optional tuning knobs -- **HTTP handlers**: `NewMux(logger, svc, config, ...)` returns `*chi.Mux` +--- -### Functional options for optional configuration (universal) +## 12. Connector OAuth2 framework -```go -// See: pkg/iam/scim/bridge/bridge.go -type Option func(*Bridge) +**Module-specific** (`pkg/connector`) but used by every third-party +integration. -func WithDryRun(dryRun bool) Option { - return func(s *Bridge) { - s.dryRun = dryRun - } -} ``` - -Used across: `pkg/agent` (WithTools, WithLogger, WithModel), `pkg/llm` (WithHTTPClient, WithBaseURL), `pkg/iam/scim` (WithGarbageCollectionInterval), `pkg/probo` (WithEvidenceDescriptionWorkerInterval). - -## Observability - -### Structured logging (universal) - -Framework: `go.gearno.de/kit/log` -- context-aware, typed fields, named loggers. - -```go -// See: pkg/iam/saml/gc.go -l.InfoCtx(ctx, "garbage collection completed", - log.Int64("deleted_assertions", deletedAssertions), - log.Int64("deleted_requests", deletedRequests), - log.Duration("duration", time.Since(start)), -) +ConnectorRegistry.Register(name, connector) + .Initiate(provider, orgID, opts, req) → redirectURL + .CompleteWithState(provider, req) → Connection + State ``` -**Never log PII, PHI, or sensitive data** -- log opaque identifiers (GIDs, request IDs) only. This is a project-wide rule. See [shared.md -- Observability](../shared.md#observability). +Three `TokenEndpointAuth` modes for OAuth2 token exchange — choose per +provider: -### OpenTelemetry tracing (universal) +- `post-form` — credentials in the form body (most providers). +- `basic-form` — Basic auth header + form body. +- `basic-json` — Basic auth header + JSON body (Notion, some others). -- GraphQL resolvers traced via `pkg/server/gqlutils/tracing.go` (TracingExtension) -- LLM calls traced with GenAI semantic conventions in `pkg/llm/llm.go` -- Agent runs traced in `pkg/agent/run.go` (span per run + per tool call) -- SCIM bridge runner traced in `pkg/iam/scim/bridge_runner.go` -- Traces exported to Grafana Tempo (OTLP on port 4317 in dev) +Provider configuration lives in `pkg/connector/providers.go` +(`providerDefinitions` map) and is applied via `ApplyProviderDefaults` +before `Register`. **Adding a new OAuth2 provider requires editing three +maps**: `providerDefinitions` (auth/token URLs), the registry registration +in `pkg/probod/probod.go`, and the GraphQL/MCP enum exposing the provider +to clients. -### No logging in pkg/coredata (universal) +The OAuth2 `state` parameter is a **stateless HMAC-signed token** carrying +`OrganizationID`, `Provider`, `RequestedScopes`, optional `ContinueURL` +and `ConnectorID` — no DB row needed. Validate with +`statelesstoken.Decode`. -The data access layer does not log. Errors are returned to callers who log at the service layer. +The HTTP client used for token exchange and connector calls **must** be +SSRF-protected (`httpclient.WithSSRFProtection()`) — see [shared.md § 12](../shared.md#12-security-baseline-cross-stack). -## Background Workers +--- -### Poll-based worker pattern (universal) +## 13. Driver pattern (accessreview) -Workers use `time.Ticker` in a `for/select` loop with `FOR UPDATE SKIP LOCKED` to claim work items. Pattern documented in `contrib/claude/go-worker.md`. +**Module-specific** (`pkg/accessreview`) — one of two registry styles in +the codebase, chosen for **explicit, switch-based** dispatch instead of +`init()`-side-effect registration. -Key elements: -- Semaphore channel for bounded concurrency -- `context.WithoutCancel` for in-flight work that must complete after shutdown -- Stale-row recovery on each tick -- Drain loop until `ErrResourceNotFound` - -Workers using this pattern: `EvidenceDescriptionWorker` (pkg/probo), `BridgeRunner` (pkg/iam/scim), `Sender` (pkg/webhook), `SendingWorker` (pkg/mailer), `Provisioner` / `Renewer` (pkg/certmanager), `GarbageCollector` (pkg/iam/saml, pkg/iam/oidc). - -### Service.Run orchestration (universal) - -Top-level `Run(ctx) error` methods start child workers as goroutines. Uses `context.WithCancelCause` for crash propagation, `context.WithoutCancel` for in-flight work isolation, and ordered shutdown with `sync.WaitGroup`. +```go +// pkg/accessreview/driver.go +func NewDriver(provider string, conn connector.Connection) (Driver, error) { + switch provider { + case "GITHUB": + return github.New(conn), nil + case "OKTA": + return okta.New(conn), nil + // ... + default: + return nil, fmt.Errorf("unsupported provider: %s", provider) + } +} +``` -See `contrib/claude/go-service.md` for the full pattern. +This is the **preferred shape** for adding a new driver of an existing +domain (auditable diff, no hidden init order). The `connector` registry +above is the only place we use post-`init` registration, and only because +provider config must be wired with secrets at startup. + +--- + +## 14. Observability + +**Universal pattern** across services and workers. + +- **Logging**: `go.gearno.de/kit/log`, always `*Ctx` variants + (`InfoCtx`, `ErrorCtx`), typed field helpers (`log.String`, `log.Error`, + `log.Duration`). Loggers are constructor-injected and derived per + subsystem with `.Named("subsystem")`. **Never** `fmt.Sprintf` into + the message — keep messages static, dynamic data in fields. + PII rules in [shared.md § 8](../shared.md#8-logging-principles-cross-stack). +- **Metrics**: Prometheus via `go.gearno.de/kit` — auto-instrumented for + HTTP servers, pg.Client, and workers. Hand-written counters/histograms + are rare and live in the subsystem that needs them. +- **Tracing**: OpenTelemetry. Tracer obtained via `tp.Tracer("subsystem")` + in `pkg/probod`, propagated through context. `pkg/llm/trace.go` adds + GenAI semantic-convention spans around every LLM call; provider + errors are recorded with `span.RecordError(err)`. + +--- + +## 15. Type system + +- **Strict throughout.** Domain types live in their owning package + (`coredata.Vendor`, `policy.Statement`); request/response types are + co-located with the service method that consumes them + (`probo.CreateVendorRequest` lives in `pkg/probo/vendor_service.go`). +- **Generics are used sparingly and intentionally**: `page.Cursor[T]`, + `page.Page[T, O]`, `api.Paginate[T]`, `errors.AsType[T]`, + `types.OrderBy[T]`. Don't introduce generics for "code reuse" alone — + the existing usages all encode a real type-level invariant. +- **Pointer types for nullable database columns** (`*string`, + `*time.Time`); double pointers (`**T`) on update-request structs to + distinguish "no change" from "set to null" (see § 2). +- **Compile-time interface assertions** at the bottom of constructors: + `var _ Interface = (*Concrete)(nil)`. diff --git a/.claude/guidelines/go-backend/pitfalls.md b/.claude/guidelines/go-backend/pitfalls.md index d218c99370..ada873aff4 100644 --- a/.claude/guidelines/go-backend/pitfalls.md +++ b/.claude/guidelines/go-backend/pitfalls.md @@ -1,240 +1,522 @@ -# Probo -- Go Backend -- Pitfalls +# Probo — Go Backend — Pitfalls -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) +> Pitfalls below are concrete: they reference a file path, the symptom, +> the cause, and a fix. Cross-stack pitfalls (PII in logs, SSRF on raw +> `http.Client`, secret rotation, URL string-formatting) are in +> [shared.md § 12](../shared.md#12-security-baseline-cross-stack) and +> [shared.md § 14](../shared.md#14-known-drift--active-violations). -## Data Access Pitfalls +--- -### Using pgx.NamedArgs instead of pgx.StrictNamedArgs +## 1. `pkg/coredata/agent_run.go:472` — hardcoded `'PENDING'` SQL literal -**What goes wrong:** Silent bugs from unset parameters. `pgx.NamedArgs` silently ignores keys present in SQL but missing from the args map. A filter parameter that should restrict results is quietly NULL, returning all rows. +**What goes wrong**: Adding a new agent-run status (or renaming +`PENDING`) silently breaks `LoadNextPendingForUpdateSkipLocked` because +the SQL string `WHERE status = 'PENDING'` is not a Go constant. -**Why it happens:** `pgx.NamedArgs` is the more commonly known type. Developers who are new to the project reach for it by default. +**Why**: This file deviates from the coredata convention of passing enum +values as named parameters. Reviewers should reject new occurrences; +the current site is documented drift to fix opportunistically. -**How to avoid:** Always use `pgx.StrictNamedArgs`. It rejects any key referenced in the SQL that is not present in the args map at runtime. Declare ALL keys in every code path -- use `nil` for inactive filters. +**Fix**: Replace the literal with a named parameter: -**Source:** Code review approval blocker (PR #845 -- "CLAUDE.md violation: pgx.NamedArgs instead of pgx.StrictNamedArgs"). See `pkg/coredata/finding_filter.go` for the correct pattern. +```go +args := pgx.StrictNamedArgs{"status": coredata.AgentRunStatusPending} +// ... WHERE status = @status ... +``` -### Conditional string building in SQLFragment() +**Source**: `pkg-coredata.json` open question; pattern violates +[`contrib/claude/coredata.md`](../../../contrib/claude/coredata.md). -**What goes wrong:** The prepared statement plan changes shape depending on runtime conditions, defeating PostgreSQL's plan cache and potentially causing subtle query-plan bugs. +--- -**Why it happens:** It feels natural to build SQL strings with Go `if` blocks, appending AND clauses conditionally. +## 2. `pkg/iam/policy` — `In()` and `NotIn()` builders documented but missing -**How to avoid:** `SQLFragment()` must return a static SQL string. Use `CASE WHEN` in the SQL itself to handle optional filters. All referenced args keys must always be declared (use `nil` for inactive ones). +**What goes wrong**: Following +`contrib/claude/authorization.md` literally, you write +`statement.When(policy.In("resource.tag", "a", "b"))`. The build fails: +no such function exists. -**Source:** Code review approval blocker (PR #845 -- "CLAUDE.md violation: SQLFragment() uses conditional string building"). See `pkg/coredata/vendor_filter.go` for the correct pattern. +**Why**: The doc references a planned API that was never implemented. +The actual surface is the `Condition` struct with +`ConditionOperator` `In` / `NotIn`. -### CTE queries missing tenant_id qualification +**Fix**: Use the struct directly: -**What goes wrong:** When a CTE joins tables that both have `tenant_id`, the outer query's `scope.SQLFragment()` produces an unqualified `tenant_id = @tenant_id` reference, causing a runtime "ambiguous column" SQL error. +```go +policy.NewStatement(...).When(policy.Condition{ + Operator: policy.ConditionOperatorIn, + Key: "resource.tag", + Values: []string{"a", "b"}, +}) +``` -**Why it happens:** The scope fragment is injected via `%s` and always produces `tenant_id = @tenant_id` without table qualification. This works fine for single-table queries but breaks multi-table CTEs. +**Source**: `pkg-iam-policy.json`; doc drift in `contrib/claude/authorization.md`. -**How to avoid:** Always include the qualified `tenant_id` column (e.g., `fi.tenant_id`) in the CTE's SELECT list so the outer scope reference is unambiguous. +--- -**Source:** Code review bug catch (PR #845 -- "Bug: CTE missing tenant_id -- will cause runtime SQL error"). +## 3. `pkg/iam/oidc` — provider `error_description` logged verbatim -### Storing tenant_id on entity structs +**What goes wrong**: An OIDC IdP returns +`error_description=user johndoe@example.com is locked`. The current +code path logs the description as-is, violating the PII-free rule. -**What goes wrong:** Violates the Scoper invariant. The coredata convention is that entity structs have no TenantID field -- tenant isolation is enforced via the Scoper at query time. Storing it on the struct creates a source of truth conflict. +**Why**: External error messages are untrusted strings that frequently +contain emails, account names, and internal hostnames. -**Why it happens:** It seems convenient to have the tenant ID on the struct for logging or passing around. One legacy entity (Vendor) has a TenantID field, which can mislead by example. +**Fix**: Log only the sanitized error code (`error=invalid_grant`). +Drop `error_description` at the boundary, or include it only at +`DEBUG` with a `redact` filter applied. See +[shared.md § 8](../shared.md#8-logging-principles-cross-stack). -**How to avoid:** Never add a TenantID field to new entity structs. Use `scope.GetTenantID()` at query time. See `pkg/coredata/scope.go`. +**Source**: `pkg-iam-oidc.json` pitfall; cross-referenced in +[shared.md § 14](../shared.md#14-known-drift--active-violations). -**Source:** Documented in `pkg/coredata/CLAUDE.md`. +--- -### Calling NoScope.GetTenantID() +## 4. `pkg/agent/tools/search` — bare `http.Client` (SSRF gap) -**What goes wrong:** Panics immediately. `NoScope` is only for cross-tenant admin queries; it has no tenant ID. +**What goes wrong**: The search tool dials user-influenced URLs with +the standard library `http.Client`. A prompt-injected URL pointing at +`http://169.254.169.254/...` (AWS metadata) or an internal Postgres +endpoint will resolve and connect. -**Why it happens:** Developers use `NoScope` for admin operations that also need to insert records (which requires `GetTenantID()`). +**Why**: SSRF protection is opt-in unless you go through +`go.gearno.de/kit/httpclient`. -**How to avoid:** Only use `NoScope` for read-only cross-tenant queries. For inserts, always use a `Scope` with a specific tenant. +**Fix**: Replace the client with +`httpclient.DefaultClient(httpclient.WithSSRFProtection())`. See +[shared.md § 12](../shared.md#12-security-baseline-cross-stack). -**Source:** `pkg/coredata/scope.go` line 56. +**Source**: `pkg-agent-tools-search.json` pitfall. -### Adding speculative database indexes +--- -**What goes wrong:** Unnecessary indexes slow down writes and increase storage. The team explicitly rejects premature indexes. +## 5. `pkg/agent/tools/security/csp.go` — missing `netcheck.ValidatePublicURL` -**Why it happens:** Coming from other projects where indexing is standard practice for new tables. +**What goes wrong**: The CSP validator fetches a URL for inspection +without checking that the URL resolves to a public address. Same SSRF +shape as #4, in a different code path. -**How to avoid:** No indexes by default. Only add when justified by observed query latency in production. Unique/constraint indexes are exempt. +**Fix**: Wrap the URL with `netcheck.ValidatePublicURL` before +dispatching, **and** use `httpclient.WithSSRFProtection()`. The two are +complementary: ValidatePublicURL is a pre-flight guard; +WithSSRFProtection runs at dial time. -**Source:** Code review (PR #917 -- "No index by default. Only if we have performance issue."). +**Source**: `pkg-agent-tools-security.json` pitfall; +also called out for `pkg/server/api/csp.go` in +[shared.md § 14](../shared.md#14-known-drift--active-violations). -### Using subqueries in SELECT instead of JOINs +--- -**What goes wrong:** Harder to read, and the query planner may not optimize them as expected. +## 6. `pkg/agent` `NavigateToURLTool` — TOCTOU on redirects -**Why it happens:** Subqueries can feel more natural when fetching a single related value. +**What goes wrong**: The tool validates the *original* URL before +fetching, but follows redirects. A malicious URL on a public host can +302-redirect to `http://10.0.0.1/...` after the validation has passed. -**How to avoid:** Prefer JOINs or CTEs for readability. Restructure complex queries to avoid SELECT-clause subqueries. +**Why**: Time-of-check / time-of-use gap between +`netcheck.ValidatePublicURL(originalURL)` and the actual dial that +follows redirects. -**Source:** Code review (PR #917 -- "Sub query in the select seams like a smell"). +**Fix**: Either disable redirects on the http client used for tool +calls (`Client.CheckRedirect = func(*http.Request, []*http.Request) error +{ return http.ErrUseLastResponse }`) and validate each hop manually, +**or** use `httpclient.WithSSRFProtection()` which validates each dial. +The latter is preferred. -## Resolver Pitfalls +**Source**: `pkg-agent.json` pitfall. -### Using panic in GraphQL resolvers +--- -**What goes wrong:** Unrecovered panics crash the server. Even recovered panics produce opaque 500 errors instead of proper GraphQL error responses. +## 7. `pkg/probod` — every child context is `context.Background()`-derived -**Why it happens:** Quick error handling shortcut, especially when copying patterns from MCP resolvers (which deliberately use MustAuthorize + panic). +**What goes wrong**: You add a new worker, call +`wg.Go(func() { worker.Run(workerCtx) })`, but forget to define and +invoke a `stopWorker()` after `<-ctx.Done()`. Process shutdown hangs +(or kills the worker mid-task with a SIGKILL after the grace period). -**How to avoid:** Always return errors in GraphQL resolvers. A dedicated cleanup PR (#865, 1425 lines) was merged to eliminate all instances. Exception: MCP resolvers use `MustAuthorize()` which deliberately panics, caught by `RecoveryMiddleware`. +**Why**: Probod **intentionally** decouples child contexts from the +parent: each worker gets a `context.Background()`-derived context with +its own cancel function. The only way it learns about shutdown is by +the caller invoking that cancel. Forgetting to call it leaves the worker +running. -**Source:** Code review approval blocker (PR #865). +**Fix**: For every `wg.Go(...)` block, define +`workerCtx, stopWorker := context.WithCancel(context.Background())` +and add `stopWorker()` to the shutdown sequence after `<-ctx.Done()`, +in the correct order (typically: stop new work in, drain, stop senders, +stop pg). -### Missing authorization before service calls in resolvers +**Source**: `pkg-probod.json` pitfall — explicitly documented at +`pkg/probod/probod.go` per the profile. -**What goes wrong:** Data is returned or modified without access control. The resolver pattern requires authorize as the very first step. +--- -**Why it happens:** Easy to forget when adding a new resolver, especially for read operations that feel harmless. +## 8. `pkg/trust` `GrantByIDs:387` — inverted `shouldSendEmail` condition -**How to avoid:** Every resolver method must call `r.authorize(ctx, resourceID, action)` as step 1. The sequence is: (1) Authorize, (2) Get service, (3) Call service, (4) Handle error. +**What goes wrong**: Grants either send a notification email when they +should not, or skip it when they should. The branch evaluates +`shouldSendEmail` with the wrong polarity. -**Source:** Documented in `pkg/server/api/console/v1/CLAUDE.md`. +**Why**: The flag was added late and the boolean expression was inverted +in the original PR. There is no test that pins the email side effect. -### Using direct service calls for entities that have dataloaders +**Fix**: Read `pkg/trust/grant.go` around line 387 and audit the +condition against the API contract. Add an e2e test that asserts the +Mailpit mailbox state for both granting paths. -**What goes wrong:** N+1 queries. If a resolver fetches an entity that has a dataloader via `prb.Entity.Get()` instead of `loaders.Entity.Load()`, each GraphQL field resolution triggers a separate database query. +**Source**: `pkg-trust.json` pitfall. -**Why it happens:** Direct service calls are more intuitive and work correctly -- they just perform badly. +--- -**How to avoid:** Check `pkg/server/api/console/v1/dataloader/dataloader.go` for the 11 entity types with batch loaders (Organization, Framework, Control, Vendor, Document, Profile, Risk, Measure, Task, File, Report). Always use the dataloader for these. +## 9. `pkg/trust` `GenerateLogoURL` — swallows errors -**Source:** Codebase pattern in `pkg/server/api/console/v1/dataloader/`. +**What goes wrong**: When the underlying URL builder fails, the function +returns an empty string without surfacing the error. Trust pages +silently render with no logo. -### Editing generated files +**Fix**: Change the signature to return `(string, error)` and propagate +the wrapped error to the caller, who can decide whether to fall back to +the default logo or fail the page render. -**What goes wrong:** All manual edits are silently overwritten next time `go generate` runs. +**Source**: `pkg-trust.json` pitfall. -**Why it happens:** The generated files (`schema/schema.go`, `types/types.go`, `server/server.go`) look like regular code and are easy to edit accidentally. +--- -**How to avoid:** Never edit files in `schema/` or `types/types.go`. After changing `schema.graphql`, run `go generate ./pkg/server/api/console/v1`. For MCP, edit `specification.yaml` then run `go generate ./pkg/server/api/mcp/v1`. +## 10. `pkg/coredata` — `*string` vs `**string` confusion in updates -**Source:** Documented in `CLAUDE.md` and all API package `CLAUDE.md` files. +**What goes wrong**: An update endpoint exposes "set field to null" but +the request struct uses `*string`. Now there is no way to distinguish +"don't change" (omit the field) from "set to null" (send null) — both +arrive as `nil`. -### Missing node resolver for types implementing Node +**Why**: Single-pointer optional fields conflate two semantics. -**What goes wrong:** The GraphQL schema declares `implements Node` but the resolver has no corresponding implementation. The API surface is incomplete -- clients cannot refetch the node by ID. +**Fix**: Use `**string` (or `**gid.GID`, `**time.Time`) on the update +request: -**Why it happens:** Adding the `implements Node` declaration to the schema is easy; implementing the resolver is a separate step that can be forgotten. +```go +type UpdateVendorRequest struct { + Name *string // single ptr: nil = no change + Description **string // double ptr: outer nil = no change, outer non-nil + inner nil = set NULL +} +``` -**How to avoid:** Every type that implements Node must have a resolver in the `node()` query path. +The validator framework auto-derefs both forms; the coredata Update +method understands both via its own check. -**Source:** Code review (PR #909 -- "It implements node but there is no node on the resolver."). +**Source**: `pkg-probo.json` pitfall; canonical reference +`pkg/probo/vendor_service.go`. -## Entity Registration Pitfalls +--- -### Reusing a removed entity type number +## 11. `pkg/probo` — 4-file checklist when adding a new entity -**What goes wrong:** GIDs stored in the database with the old type number will be misinterpreted as the new type, corrupting entity resolution. +**What goes wrong**: You add a new domain entity (`Asset` say), wire it +in `pkg/coredata`, write a service, expose a GraphQL field — and IAM +silently denies access for every role. Or worse, allows it for every +role because no policy matched. -**Why it happens:** The gap in the entity type registry (`entity_type_reg.go`) looks like it should be filled. +**Why**: A new entity touches **four** files outside the coredata pair +and the service file: -**How to avoid:** Removed types are tombstoned as `_ uint16 = N // EntityType - removed`. Always append new types at the end. Never fill gaps. +| File | What to add | +| --- | --- | +| `pkg/probo/actions.go` | `ActionAssetCreate / Read / Update / Delete / List` constants | +| `pkg/probo/policies.go` | Allow statements per role, with `organizationCondition` | +| `pkg/coredata/entity_type_reg.go` | Next sequential `uint16` constant + `case` in `NewEntityFromID` | +| `pkg/probo/service.go` | New `*AssetService` field on `TenantService` + wiring in `WithTenant` | -**Source:** `pkg/coredata/entity_type_reg.go` lines 34, 54, 58, 70. +Plus, when the new entity should publish webhooks, add a DTO file in +`pkg/webhook/types/asset.go` with a `NewAsset(coredata.Asset)` +constructor. -### Forgetting to register a new entity in NewEntityFromID +**Fix**: Use the entity checklist above when scaffolding. The +authorization rule tells you which file you missed: a denied role with +no policy matched the action; an over-permitted action means the +`organizationCondition` is missing. -**What goes wrong:** The function silently returns `(nil, false)` for unregistered types. Authorization and node resolution break silently. +**Source**: `pkg-probo.json` pitfall and +`contrib/claude/authorization.md` §"New entity IAM wiring". -**Why it happens:** Adding the const is one step; adding the switch case is a separate step in the same file. +--- -**How to avoid:** When adding a new entity type constant, always add the corresponding case in the `NewEntityFromID` switch in the same file. +## 12. `pkg/llm/anthropic` — Anthropic requires `MaxTokens` -**Source:** `pkg/coredata/entity_type_reg.go`. +**What goes wrong**: `ChatCompletion` returns +`llm.ErrContextLength` immediately, with no API call made. -## Authentication Middleware Pitfalls +**Why**: Anthropic's API requires `max_tokens` to be set. The Probo +adapter validates this client-side; OpenAI does not require it. -### Wrong middleware order +**Fix**: Always populate `ChatCompletionRequest.MaxTokens` for any +request that may be routed to Anthropic. Default in +`pkg/probod/llm.go` is 4096. -**What goes wrong:** Authentication checks fail silently or mutual exclusion checks miss. +**Source**: `pkg-llm.json`; mapped errors in `pkg/llm/anthropic/provider.go`. -**Why it happens:** The three middlewares (session, API key, identity presence) depend on a specific ordering. +--- -**How to avoid:** The canonical order is: `NewSessionMiddleware` -> `NewAPIKeyMiddleware` -> `NewIdentityPresenceMiddleware`. Identity presence must always be last -- it only checks context, it does not perform authentication. +## 13. `pkg/llm` streams — `stream.Close()` must always be called -**Source:** `pkg/server/api/console/v1/resolver.go` lines 89-91. +**What goes wrong**: A streaming call returns; the caller breaks out of +the loop on an early error and forgets to call `Close()`. The HTTP +connection leaks (held until socket timeout) and the OTel span never +ends. -## Service Layer Pitfalls +**Why**: `tracedStream` ends the span via `sync.Once` triggered by +either stream exhaustion **or** `Close()`. Skipping both leaks both. -### Over-engineered service logic +**Fix**: -**What goes wrong:** Code review rejects the PR for unnecessary complexity. +```go +stream, err := client.ChatCompletionStream(ctx, req) +if err != nil { return fmt.Errorf("cannot start stream: %w", err) } +defer stream.Close() // <-- mandatory -**Why it happens:** Developers add defensive patterns (e.g., "if updated" checks) that don't match the project's straightforward style. +for stream.Next() { ... } +if err := stream.Err(); err != nil { return err } +``` -**How to avoid:** Keep service methods simple and direct. If reviewers say "useless complex logic," simplify. The team values clarity over defensive programming. +**Source**: `pkg-llm.json`; pattern in `pkg/llm/trace.go`. -**Source:** Code review (PR #898 -- "useless complex logic" and "We don't use this 'if updated' pattern in any other updates."). +--- -## Validation Pitfalls +## 14. `pkg/webhook` — DTO discipline has no compile-time guard -### Forgetting Required() on mandatory fields +**What goes wrong**: `webhook.InsertData(ctx, tx, scope, orgID, "vendor:created", v)` +where `v` is a `*coredata.Vendor`. It compiles, runs, and the customer +receives a payload with internal field names (`tenant_id`, `db_*`), +deprecation-prone column names, and accidental sensitive fields. -**What goes wrong:** All validators except `Required()` silently pass nil values. A mandatory field with only `SafeText()` will pass validation when nil. +**Why**: `InsertData` accepts `any` for the payload and JSON-marshals +it. There is no type seam preventing coredata structs from leaking. -**Why it happens:** The design philosophy is "nil = optional." This is the opposite of most validation libraries. +**Fix**: Always wrap with `webhooktypes.NewXxx(coredata)`: -**How to avoid:** Always include `Required()` in the validator chain for mandatory fields. For optional fields, the nil-passing behavior is intentional. +```go +webhook.InsertData(ctx, tx, scope, orgID, "vendor:created", webhooktypes.NewVendor(v)) +``` -**Source:** Documented in `contrib/claude/validation.md` and tested in `pkg/validator/validation_test.go`. +If a DTO doesn't exist for a new entity, **add one** in +`pkg/webhook/types/.go` with a `NewXxx(coredata.Xxx) Xxx` +constructor. Reviewers enforce this — frequency-2 rule (PR #720). -### Using google/uuid instead of gearno.de/crypto/uuid +**Source**: `pkg-webhook.json`; review evidence +[shared.md § 13 #13](../shared.md#13-code-review-enforced-standards). -**What goes wrong:** Build may work but code review will reject it. The project explicitly bans `github.com/google/uuid`. +--- -**Why it happens:** `google/uuid` is the most common Go UUID library and appears first in search results. +## 15. `pkg/connector` — registering a new OAuth2 provider edits 3 maps -**How to avoid:** Always use `go.gearno.de/crypto/uuid`. For entity IDs, use `gid.New(tenantID, entityType)` instead of UUIDs. +**What goes wrong**: You add `LINEAR` to `providerDefinitions` but +forget to wire `ConnectorRegistry.Register("LINEAR", ...)` in +`pkg/probod/probod.go`, or you forget to expose the provider name in +the GraphQL/MCP enum. Either omission yields a runtime "unknown +provider" error. -**Source:** Documented in `CLAUDE.md`. +**Fix**: Three locations, lockstep: -## Filter Design Pitfalls +1. `pkg/connector/providers.go` — `providerDefinitions["LINEAR"] = {...}`. +2. `pkg/probod/probod.go` — call `Register("LINEAR", connector)` after + `ApplyProviderDefaults` with the deployment's client ID/secret. +3. The connector-provider GraphQL enum (and MCP equivalent) — add + `LINEAR` to keep the four-surface rule (see + [shared.md § 3](../shared.md#3-the-four-surface-api-rule)). -### Using boolean flags instead of State[] slices for multi-state filtering +**Source**: `pkg-connector.json` and `contrib/claude/api-surface.md`. -**What goes wrong:** Combinatorial flag proliferation. Adding a third state requires exponential flag combinations. +--- -**Why it happens:** Boolean flags seem simpler for two-state filtering. +## 16. `pkg/page` — `CursorKey.Value` is `any`, JSON round-trips can change `int` → `float64` -**How to avoid:** Use a `State[]` slice filter approach for multi-state queries. This allows flexible multi-state queries without combinatorial growth. +**What goes wrong**: A cursor key is constructed with an +`int` sort value. After serialising to JSON and reading it back from +the client, the `Value` field is `float64`. Comparing +`reflect.DeepEqual(original, decoded)` fails, or worse, the +SQL parameter becomes a float and the keyset comparison silently +broadens. -**Source:** Code review (PR #855 -- "let refactor to have State[] and filter by state instead of that"). +**Why**: `CursorKey` stores `any`, and Go's `encoding/json` +defaults all numbers to `float64` on decode. The base64-JSON round trip +inherits the loss. -### Using virtual/computed columns instead of real columns +**Fix**: When parsing a cursor key from a client, normalise the value +to the expected concrete type before using it as a SQL parameter: -**What goes wrong:** Virtual columns computed in SELECT are harder to maintain and don't participate in indexes. +```go +ck, err := page.ParseCursorKey(s) +// ... type-switch ck.Value to coerce float64 → int when expected ... +``` -**Why it happens:** Feels efficient to derive a value in the query rather than adding a column. +In practice, sort values are usually `time.Time` (passes through as +RFC3339 string) or `string`; the int trap appears with numeric sort +fields. -**How to avoid:** Prefer adding a real database column to the schema rather than computing a derived value in SELECT. +**Source**: `pkg-page.json`. -**Source:** Code review (PR #855 -- "Let put a real column here, instead of virtual one like that"). +--- -## GraphQL Schema Pitfalls +## 17. `pkg/probod` — `runTrustCenterServer` errgroup vs top-level WaitGroup -### Missing default filter on Organization query fields +**What goes wrong**: You "fix" the inconsistency by porting +`runTrustCenterServer` to the top-level `sync.WaitGroup` pattern. The +ACME provisioner can now run when the HTTP server has crashed, and +crash-fast semantics are lost. -**What goes wrong:** The query returns an expensive unfiltered result set. +**Why**: The errgroup is **intentional** for this subsystem. The cert +provisioner, cert renewer, HTTP server, and HTTPS server form a unit: +if any of them dies, the others must die too. `errgroup.WithContext` +encodes that. -**Why it happens:** Fields on Organization that accept a filter argument are added without specifying a default. +**Fix**: Leave the errgroup in place. Document it locally if you touch +the file. Do not adopt errgroup at the top level — the rest of probod +deliberately separates child lifetimes. -**How to avoid:** Always provide a default filter value in the schema: `filter: SomeFilter = { snapshotId: null }`. +**Source**: `pkg-probod.json` pitfall. -**Source:** Code review (PR #845 -- "Bug: Missing default snapshot filter on 'findings' field"). +--- -### Audit log FK missing ON DELETE CASCADE +## 18. `cmd/` — adding a new binary requires three lockstep edits -**What goes wrong:** Deleting an organization fails because audit log rows still reference it. +**What goes wrong**: You add `cmd/probo-migrate-thing/main.go`, build +locally, and ship. CI passes, the release pipeline runs, and the new +binary is missing from the release archives and the cross-platform +build matrix. -**Why it happens:** Default FK behavior is RESTRICT. +**Why**: Three integration points outside the package: -**How to avoid:** Audit log tables should use `ON DELETE CASCADE` for the parent organization FK so deletion cascades automatically. +1. **`cmd//main.go`** itself. +2. **`GNUmakefile`** — append the binary to the build matrix and the + default `make build` target. +3. **GoReleaser CI config** — extend the build/archive entries so the + new binary is signed, packaged, and uploaded. -**Source:** Code review (PR #909 -- "We want if we delete the org we don't need keep those logs."). +**Fix**: Use an existing binary's diff (e.g. `probod-bootstrap`'s +introduction commit) as a checklist. See +[shared.md § 7](../shared.md#7-ci--quality-gates). + +**Source**: `cmd.json` open question. + +--- + +## 19. `pkg/iam/oidc`, `pkg/iam/oauth2server`, PKCE — 100% coverage required + +**What goes wrong**: A new helper in `pkg/iam/oidc/idtoken.go` ships +without unit tests. PR is blocked by a frequency-3 reviewer comment: +*"this file must have unit test 100%"*. + +**Why**: Auth-sensitive packages are reviewed line-by-line and +covered exhaustively — token issuance, signing, verification, PKCE, +state cleanup. A regression here is a security incident. + +**Fix**: Cover every branch and every error path with table-driven +unit tests. Use `httptest.NewServer` plus +`httpclient.WithSSRFAllowLoopback()` for IdP fakes (the connector tests +are a good template). See [testing.md § 5](./testing.md#5-coverage-expectations). + +**Source**: [shared.md § 13 #11](../shared.md#13-code-review-enforced-standards). + +--- + +## 20. `pkg/iam/oauth2server` — failed PKCE must clean up the auth code + +**What goes wrong**: The code-challenge check fails. The handler returns +an error to the client but leaves the authorization code valid, so a +second request can succeed by replaying the code without the +challenge. + +**Why**: PR #957 reviewer comment: *"Security issue, if the code +challenge failed it will not delete the code."* + +**Fix**: On every PKCE failure path, delete the auth-code row inside +the same transaction as the failure response. See +[shared.md § 12](../shared.md#12-security-baseline-cross-stack). + +**Source**: PR-mining; cross-stack rule applied to this stack. + +--- + +## 21. Forgetting `default:` in resolver error switches + +**What goes wrong**: A new error type introduced by `pkg/probo` falls +through every `case errors.Is(...)` arm. Without `default:`, gqlgen's +default error formatter forwards the original error to the client — +SQL details, file paths, stack info, all on the wire. + +**Fix**: Every resolver error switch ends with: + +```go +default: + r.logger.ErrorCtx(ctx, "cannot ", log.Error(err)) + return nil, gqlutils.Internal(ctx) +} +``` + +See [patterns.md § 7](./patterns.md#7-graphql-resolver-shape) and +[shared.md § 3](../shared.md#3-the-four-surface-api-rule). + +--- + +## 22. Forgetting `@goModel` on a Connection type + +**What goes wrong**: `vendorConnection.totalCount` always returns 0, +or the dispatch panics with a nil `Resolver` field. + +**Why**: Without `@goModel`, gqlgen generates a plain struct lacking +the `Resolver` and `ParentID` fields that the totalCount dispatcher +relies on. + +**Fix**: In the `.graphql` file for the entity, add: + +```graphql +type VendorConnection @goModel(model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.VendorConnection") { + edges: [VendorEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} +``` + +**Source**: `pkg-server-api-console.json` pitfall. + +--- + +## 23. Forgetting to run `go generate` after a schema/spec change + +**What goes wrong**: Resolver compile errors after editing a `.graphql` +file (or `mcp specification.yaml`), or — worse — the build succeeds +but the server returns "field not found" because `schema.go` is stale. + +**Fix**: After every edit to `.graphql` schema files or +`pkg/server/api/mcp/v1/specification.yaml`: + +```bash +go generate ./pkg/server/api/console/v1 # or connect/v1, trust/v1 +go generate ./pkg/server/api/mcp/v1 +make relay # for frontend GraphQL ops +``` + +CI catches stale generated files; commit the regenerated output. See +[shared.md § 2](../shared.md#2-build--toolchain). + +--- + +## 24. `coredata.NewNoScope()` accidentally used for tenant-bound work + +**What goes wrong**: A worker that operates per-tenant uses +`coredata.NewNoScope()`. The first INSERT panics at runtime (NoScope's +`GetTenantID()` panics), or — worse — a SELECT returns rows from every +tenant. + +**Why**: NoScope is a deliberate escape hatch for cross-tenant admin +queries (e.g. claiming the next pending row across all tenants). It +**panics** on `GetTenantID()` to prevent insert with no tenant. + +**Fix**: After claiming a tenant-owned row with NoScope, switch to +`coredata.NewScopeFromObjectID(row.ID)` for any subsequent +per-tenant work. PR #957 explicitly removed accidental NoScope usage: +*"remove use coredata.NewNoScope() where needed"*. See +[shared.md § 9](../shared.md#9-tenant-isolation--cross-stack-architectural-principle). + +**Source**: `pkg-coredata.json` and PR-mining. diff --git a/.claude/guidelines/go-backend/testing.md b/.claude/guidelines/go-backend/testing.md index 2ea2295f08..f64cc7b76c 100644 --- a/.claude/guidelines/go-backend/testing.md +++ b/.claude/guidelines/go-backend/testing.md @@ -1,319 +1,346 @@ -# Probo -- Go Backend -- Testing +# Probo — Go Backend — Testing -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) +> Cross-stack rules (PII in test data, license headers, CI gates) live in +> [shared.md](../shared.md). This file documents Go test mechanics: unit +> tests, e2e tests, factory builders, RBAC matrix tests, tenant isolation, +> pagination assertions, and coverage expectations. +> +> Authoritative references: +> [`contrib/claude/go-testing.md`](../../../contrib/claude/go-testing.md), +> [`contrib/claude/e2e.md`](../../../contrib/claude/e2e.md). -## Framework & Tooling +--- -- **Test runner**: `gotestsum` wrapping `go test` (used in CI) -- **Assertions**: `github.com/stretchr/testify` (`require` for fatal, `assert` for non-fatal) -- **Race detection**: `-race` flag always enabled (`make test` includes it) -- **Coverage**: `-cover -coverprofile=coverage.out` (CI generates HTML reports) -- **Build command**: `make test` (all tests) or `make test MODULE=./pkg/foo` (single module) -- **E2E command**: `make test-e2e` (requires `bin/probod` built first) -- **Verbose**: `make test-verbose` +## 1. Frameworks -## Test Organization & File Naming +| Layer | Framework | Used by | +| --- | --- | --- | +| Unit & e2e assertions | `github.com/stretchr/testify` (`require` + `assert`) | every package with tests | +| Integration target | `make test-e2e` against live `probod` + Docker Compose stack | `e2e/console`, `e2e/mcp` | +| Mail assertions | Mailpit (HTTP API at `GetMailpitBaseURL()`) | `e2e/internal/testutil/mailpit.go` | +| Random data | `github.com/brianvoe/gofakeit/v7` via `factory.SafeName`/`SafeEmail` | factories | +| OAuth2 / HTTP fakes | `httptest.NewServer` + `httpclient.WithSSRFAllowLoopback()` | `pkg/connector` | -### Package naming (universal) +> **No Ginkgo / GoConvey / mock libraries.** Plain `*testing.T`, +> testify, and table-driven `t.Run`. -Black-box test packages (`package foo_test`) are preferred. White-box packages (`package foo`) are used only when testing unexported functions. +--- -```go -// Black-box (preferred) -- See: pkg/iam/policy/example_test.go -package policy_test - -// White-box (only for unexported) -- See: pkg/iam/policy/evaluator_test.go -package policy -``` - -### Test naming (universal) +## 2. Unit-test mechanics -Top-level: `TestFunctionName_Scenario` or `TestEntity_Operation`. -Subtests: `t.Run` with lowercase descriptive names. +### Mandatory `t.Parallel()` at every level ```go -// See: e2e/console/vendor_test.go -func TestVendor_Create(t *testing.T) { +func TestVendor_Validate(t *testing.T) { t.Parallel() - t.Run("with full details", func(t *testing.T) { - t.Parallel() + cases := []struct { + name string + request CreateVendorRequest + wantErr bool + }{ + {"required name missing", CreateVendorRequest{}, true}, // ... - }) + } - t.Run("viewer cannot create vendor", func(t *testing.T) { - t.Parallel() - // ... - }) + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + err := c.request.Validate() + if c.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } } ``` -### Parallel execution (universal, enforced in code review) +Every test function and **every** subtest calls `t.Parallel()`. The CI +runs with `-race`; missing `t.Parallel()` is called out in review. -**Always call `t.Parallel()` at both the top-level test function and every subtest.** Missing `t.Parallel()` calls in subtests are flagged as CLAUDE.md violations and are an **approval blocker**. See [shared.md -- Approval blockers](../shared.md#approval-blockers-will-block-merge). +### `require` vs `assert` -### Test file location (universal) +- **`require.*`** — fail-fast preconditions. The first failure aborts the + test. Use for: `require.NoError(t, err)`, factory creation, "the row + must exist" guards. +- **`assert.*`** — collecting assertions that should keep running. Use + for value comparisons after the precondition has been met: + `assert.Equal(t, want, got)`, `assert.Len(t, list, 3)`, + `assert.True(t, found)`. -- **Unit tests**: co-located as `_test.go` in the same package -- **E2E tests**: one file per entity in `e2e/console/` (package `console_test`) +The mix is intentional: `require` proves you have a valid object to +inspect; `assert` then inspects all of its fields without short-circuiting +on the first mismatch. -### Mock definitions (universal) +### Black-box test packages -Define mock types and helpers at the top of the test file, not inline. Mocks implement the interface under test. +Tests live in **`package _test`** by default — frequency-2 +reviewer rule (PR #1023). This forces tests to use the public API and +prevents creeping reliance on internals. See +[conventions.md § 11](./conventions.md#11-test-packages). -```go -// See: pkg/llm/llm_test.go -type mockProvider struct { - chatCompletionFunc func(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) -} +### File naming -func (m *mockProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) { - return m.chatCompletionFunc(ctx, req) -} -``` +- `_test.go` next to `.go`. +- Test function name: `Test_` (e.g. `TestVendor_Validate`, + `TestCookieBanner_LoadByOrganizationID_WithFilter`). +- Subtest names: lowercase descriptive strings — + `"with full details"`, `"viewer cannot create"`, + `"unknown model returns false"`. -## require vs assert +### Table-driven shape (canonical) -This distinction is critical and enforced across the codebase: +`pkg/llm/registry_test.go` and `pkg/connector/oauth2_test.go` are good +references for table-driven testify tests with `t.Parallel()` at both +levels. -| Function | Library | Use when | Behavior | -|----------|---------|----------|----------| -| `require.*` | `testify/require` | Preconditions that must succeed | Stops the test immediately on failure | -| `assert.*` | `testify/assert` | Value verification | Continues test after failure | +--- -```go -// See: e2e/console/vendor_test.go -// require for preconditions -- test cannot continue without these -result, err := c.Execute(t, query, variables) -require.NoError(t, err) +## 3. E2E mechanics -// assert for value checks -- continue to check more fields -assert.Equal(t, name, result.Data.CreateVendor.Vendor.Name) -assert.Equal(t, description, result.Data.CreateVendor.Vendor.Description) -``` - -Common require functions: `NoError`, `Error`, `ErrorAs`, `Len`, `NotNil`. -Common assert functions: `Equal`, `Contains`, `True`, `False`, `Nil`. - -## End-to-End Testing - -### Architecture +E2E tests live in `e2e/console/` (~43 files), `e2e/mcp/` (~22 files), +plus the shared `e2e/internal/testutil` and `e2e/internal/factory`. +They run against a real `probod` binary backed by the Docker Compose +stack (`make stack-up && make test-e2e`). -E2E tests run against a live `bin/probod` subprocess with a real PostgreSQL database. `testutil.Setup()` starts the server once per test run via `sync.Once`. Each test creates isolated organizations and users, so tests never share state. - -### Test structure - -Every e2e test follows this pattern: +### Client setup ```go -// See: e2e/console/vendor_test.go func TestVendor_Create(t *testing.T) { t.Parallel() - // 1. Create test client (isolated org + user) - owner := testutil.NewClient(t, testutil.RoleOwner) - - // 2. Create test data via factory - name := factory.SafeName("vendor") - - // 3. Execute GraphQL mutation - result, err := owner.Execute(t, createVendorQuery, map[string]any{ - "input": map[string]any{"name": name}, - }) - require.NoError(t, err) - - // 4. Assert results - assert.Equal(t, name, result.Data.CreateVendor.Vendor.Name) + owner := testutil.NewClient(t, testutil.RoleOwner) // signs up a user, creates an org + viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) // joins same org via Mailpit invite flow + // ... } ``` -### Test data factories +- `testutil.NewClient(t, role)` provisions a fresh user **and** a fresh + organization. Returns a `*testutil.Client` carrying cookie-jar HTTP + transport, `OrganizationID`, `UserID`, `ProfileID`, `Role`. +- `testutil.NewClientInOrg(t, role, owner)` provisions a new user in an + existing org. Drives the full **invite → Mailpit lookup → activate → + set password** sequence. Tests that need multiple roles in the same + org use this. +- Each test gets its own org — there is no shared fixture data. + Parallel tests are isolated by tenant scoping in the daemon. -Two patterns for creating test data: +### Factory builders (two-tier) ```go -// Simple function -- See: e2e/internal/factory/factory.go -vendor := factory.CreateVendor(t, client) - -// Fluent builder -- See: e2e/internal/factory/factory.go -vendor := factory.NewVendor(client). - WithName("Custom Name"). - WithDescription("Custom description"). - Create(t) +// Builder pattern — preferred when the test reads the entity multiple times +v := factory.NewVendor(owner). + WithName(factory.SafeName("vendor")). + WithDescription("imported from CSV"). + Create() + +// Flat form — preferred for one-liners +vid := factory.CreateVendor(owner, factory.Attrs{"name": factory.SafeName("vendor")}) ``` -Use `factory.SafeName(prefix)` for unique names and `factory.SafeEmail()` for unique emails. These use gofakeit to prevent collisions in parallel tests. - -### RBAC testing (universal -- required for every entity) +- Both forms live under `e2e/internal/factory/`. +- **`factory.SafeName(prefix)`** and **`factory.SafeEmail()`** generate + unique values per call (gofakeit-backed) — required because tests run + in parallel against a shared database. +- `factory.Attrs` is `map[string]any` with typed accessor methods + (`getString`, `getInt`, `getBool`, `getStringPtr`). +- Adding a new entity to the factory: implement both forms in the same + file (`e2e/internal/factory/.go`). -Every entity must test role-based access control: +### GraphQL transport ```go -// See: e2e/console/vendor_test.go -t.Run("viewer cannot create vendor", func(t *testing.T) { - t.Parallel() - viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - _, err := viewer.Execute(t, createVendorQuery, variables) - testutil.RequireForbiddenError(t, err) -}) -``` +type vendorResponse struct { + Node struct { + ID string + Name string + } +} -Test all role combinations: Owner, Admin, Viewer for create/update/delete/read. +var resp vendorResponse +err := owner.Execute(t, ctx, vendorQuery, map[string]any{"id": vid}, &resp) +require.NoError(t, err) +assert.Equal(t, vid, resp.Node.ID) +``` -### Tenant isolation testing (universal -- required for every entity) +- `Client.Execute` → unmarshals into the response struct, fails fast on + GraphQL errors. +- `Client.Do` → returns the raw `GraphQLResponse` for tests that need + to inspect partial results. +- `Client.ExecuteShouldFail` → expects errors, returns the typed + `GraphQLErrors` slice (each carries `extensions.code`). +- `Client.ExecuteConnect` → hits the `connect` GraphQL endpoint + (`/api/connect/v1/graphql`). +- `Client.ExecuteWithFile` → multipart upload following the + graphql-multipart-request-spec. -Every entity must verify that cross-organization access is denied: +### Inline response structs -```go -// See: e2e/console/vendor_test.go -t.Run("cannot read vendor from another organization", func(t *testing.T) { - t.Parallel() - otherOwner := testutil.NewClient(t, testutil.RoleOwner) - testutil.AssertNodeNotAccessible(t, otherOwner, vendor.ID) -}) -``` +E2E tests use **inline anonymous structs** for GraphQL response shapes +inside each test function. The `console` and `mcp` test packages do +not import generated TypeScript-style types. Keeping the struct local +makes the test self-documenting and avoids cross-test coupling. -### Assertion helpers +--- -| Helper | Purpose | -|--------|---------| -| `testutil.RequireForbiddenError(t, err)` | Verify RBAC denial | -| `testutil.RequireErrorCode(t, err, code)` | Check specific GraphQL error code | -| `testutil.AssertTimestampsOnCreate(t, created, updated, before)` | Verify createdAt == updatedAt on create | -| `testutil.AssertTimestampsOnUpdate(t, created, updated, origCreated, origUpdated)` | Verify updatedAt advances on update | -| `testutil.AssertFirstPage(t, pageInfo)` | Verify first page of pagination | -| `testutil.AssertLastPage(t, pageInfo)` | Verify last page of pagination | -| `testutil.AssertNodeNotAccessible(t, client, id)` | Verify tenant isolation | -| `testutil.AssertOrderedAscending(t, items)` | Verify sort order | +## 4. Test patterns -### Inline GraphQL queries +### RBAC matrix tests -E2E tests define GraphQL queries as package-level `const` strings with typed result structs: +For every mutating endpoint, parametrise over roles to assert that +authorization is enforced: ```go -// See: e2e/console/vendor_test.go -const createVendorQuery = ` - mutation CreateVendor($input: CreateVendorInput!) { - createVendor(input: $input) { - vendor { - id - name - description - createdAt - updatedAt +func TestVendor_Create_RBAC(t *testing.T) { + t.Parallel() + + cases := []struct { + role testutil.TestRole + canCreate bool + }{ + {testutil.RoleOwner, true}, + {testutil.RoleAdmin, true}, + {testutil.RoleEmployee, false}, + {testutil.RoleViewer, false}, + {testutil.RoleAuditor, false}, + } + + for _, c := range cases { + c := c + t.Run(string(c.role), func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + actor := testutil.NewClientInOrg(t, c.role, owner) + + err := actor.Execute(t, ctx, createVendorMutation, vars, nil) + if c.canCreate { + require.NoError(t, err) + } else { + testutil.RequireForbiddenError(t, err) } - } + }) } -` - -type createVendorResult struct { - Data struct { - CreateVendor struct { - Vendor struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - } `json:"vendor"` - } `json:"createVendor"` - } `json:"data"` } ``` -### Validation scenario testing +- **`testutil.RequireForbiddenError(t, err)`** asserts the error has + a `FORBIDDEN` extensions.code. +- **`testutil.RequireErrorCode(t, err, "INVALID")`** for any other + category (`NOT_FOUND`, `INVALID`, `CONFLICT`, `UNAUTHENTICATED`). -Table-driven tests for validation (HTML injection, control chars, max length): +### Tenant isolation ```go -// See pattern from contrib/claude/e2e.md -tests := []struct{ - name string - input string - code string -}{ - {"html injection", "", "UNSAFE_CONTENT"}, - {"control characters", "name\x00with\x01control", "INVALID_FORMAT"}, - {"exceeds max length", strings.Repeat("a", 101), "TOO_LONG"}, -} +func TestVendor_TenantIsolation(t *testing.T) { + t.Parallel() -for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - // ... - testutil.RequireErrorCode(t, err, tt.code) - }) + owner1 := testutil.NewClient(t, testutil.RoleOwner) + owner2 := testutil.NewClient(t, testutil.RoleOwner) // different org + + vid := factory.CreateVendor(owner1, factory.Attrs{"name": factory.SafeName("v")}) + + // Owner of org 2 must not see org 1's vendor. + testutil.AssertNodeNotAccessible(t, owner2, vid) } ``` -## Unit Testing Patterns - -### Validator package (module-specific -- uses stdlib testing) +- **Two `testutil.NewClient` calls** create two independent organizations. +- **`testutil.AssertNodeNotAccessible(t, client, gid)`** accepts both a + nil node response and a non-nil error — both are valid access-denied + signals from the GraphQL `node(id: ID!)` field. +- Apply this to every entity that has a public ID. It is the single + most important regression test for the `Scoper` invariant. -The `pkg/validator` package uses stdlib `testing` without testify. All other packages use testify. +### Pagination assertions ```go -// See: pkg/validator/validation_test.go -func TestOptionalByDefault(t *testing.T) { - v := New() - v.Check((*string)(nil), "field", MinLen(1)) - if v.Error() != nil { - t.Errorf("expected nil pointer to pass non-Required validators") - } -} +testutil.AssertFirstPage(t, pageInfo) // hasPrev=false, hasNext=true +testutil.AssertMiddlePage(t, pageInfo) // hasPrev=true, hasNext=true +testutil.AssertLastPage(t, pageInfo) // hasPrev=true, hasNext=false ``` -### Policy package (module-specific -- uses Go example tests) - -`pkg/iam/policy` uses Go example tests with `Output:` assertions for documentation-as-tests: +For ordering: ```go -// See: pkg/iam/policy/example_test.go -func Example_basicAuthorization() { - p := policy.NewPolicy("test", - policy.Allow("service:resource:read"), - ) - // ... - fmt.Println(result.Decision) - // Output: allow -} +testutil.AssertOrderedAscending(t, ids) +testutil.AssertTimesOrderedDescending(t, timestamps) ``` -### LLM/Agent tests (module-specific -- mock providers) +For timestamps on create/update flows: -Tests for `pkg/llm` and `pkg/agent` use mock implementations of the Provider and ChatCompletionStream interfaces: +```go +testutil.AssertTimestampsOnCreate(t, createdAt, updatedAt) +testutil.AssertTimestampsOnUpdate(t, before, after) +``` + +### Mailpit-backed assertions ```go -// See: pkg/llm/llm_test.go -func newTestClient(provider llm.Provider) (*llm.Client, *tracetest.SpanRecorder) { - recorder := tracetest.NewSpanRecorder() - tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) - client := llm.NewClient("test-system", provider, - llm.WithTracerProvider(tp), - ) - return client, recorder -} +mails, err := owner.SearchMails(t, ctx, "to:"+inviteeEmail) +require.NoError(t, err) +require.Len(t, mails, 1) + +links := owner.CheckMessageLinks(t, ctx, mails[0].ID) +activationURL := links[0] +// ...drive activation flow... ``` -## Coverage Expectations +Used in invite tests, password-reset tests, document-published +notifications, vendor-assessment dispatch, etc. + +--- + +## 5. Coverage expectations + +- **Default**: there is no project-wide coverage threshold. CI emits + `coverage.out` but does not fail on a percentage. +- **Auth-sensitive code requires 100% unit-test coverage.** + Frequency-3 reviewer rule (PR #957): *"this file must have unit test + 100%"*. This applies to: + - `pkg/iam/oauth2server/` + - `pkg/iam/oidc/` + - `pkg/iam/saml/` + - PKCE / ID-token verification helpers + - any new file that issues, validates, or rotates tokens +- **New features need at least one e2e test in `e2e//`.** + Frequency-2 reviewer rule (PR #1102: *"Maybe add some e2e tests?"*). + The minimum bar is one happy-path test per new mutation; richer + coverage (RBAC matrix, tenant isolation, pagination) is encouraged. + +--- + +## 6. Running tests + +```bash +# All Go unit tests with race detector and coverage +make test +# One package +make test MODULE=./pkg/llm + +# E2E suite (requires Docker Compose stack up + Lima sandbox if +# you do not have a Linux host) +make stack-up +make test-e2e +``` -No explicit coverage threshold is enforced. The CI pipeline generates coverage reports (`coverage.out`, HTML). Several key packages have no unit tests at all (pkg/coredata, pkg/trust, pkg/iam/oidc, pkg/server/api/authn) -- their coverage comes exclusively from e2e tests. +E2E target reads `GetBaseURL()` and `GetMailpitBaseURL()` from env vars +set by the Make target — see `e2e/internal/testutil/env.go`. -The project philosophy is that e2e tests against the live server provide the most valuable coverage for the API layer, while unit tests focus on pure logic packages (validator, policy, llm, agent). +--- -## New Entity E2E Checklist +## 7. A complete example -From `contrib/claude/e2e.md`, every new entity must have tests covering: +`e2e/console/vendor_test.go` is the canonical reference: it covers +create / update / delete / list / view, RBAC matrix, tenant isolation, +pagination, timestamp assertions, and webhook side-effects, all using +the conventions above. When in doubt about a new e2e test, copy its +shape. -1. Create with full details -2. Create with minimal details -3. Update all fields -4. Update single field -5. Delete -6. List with pagination (first page, next page, ordering) -7. RBAC: owner/admin can create, viewer cannot -8. RBAC: owner/admin can update, viewer cannot -9. RBAC: owner/admin can delete, viewer cannot -10. Tenant isolation: cross-org user cannot access resource -11. Timestamps: createdAt == updatedAt on create, updatedAt advances on update +For unit testing, `pkg/llm/registry_test.go` shows the table-driven +form, `pkg/connector/oauth2_test.go` shows the `httptest` + state-token +fake-server form, and `pkg/page/cursor_test.go` shows the +generics-with-test-stub form (`testOrderField` satisfies the +`OrderField` constraint for isolation). diff --git a/.claude/guidelines/shared.md b/.claude/guidelines/shared.md index 9bf79a8003..1c0286dd1d 100644 --- a/.claude/guidelines/shared.md +++ b/.claude/guidelines/shared.md @@ -1,362 +1,592 @@ -# Probo -- Shared Conventions +# Probo — Shared Conventions + +> Auto-generated by potion-skill-generator. Last generated: 2026-05-01. +> The authoritative source of truth for every subsystem rule lives in `contrib/claude/`. +> This file distills the cross-cutting concerns that apply to **both** the Go backend +> and the TypeScript frontend. Stack-specific rules belong in +> `.claude/guidelines/{go-backend,typescript-frontend}/`. +> Sections wrapped in `` are preserved during refresh. + +--- + +## 1. Project Overview + +Probo is an **open-source compliance platform** (MIT-licensed product, ISC-licensed source files). +The repo is a **polyglot monorepo** combining a Go backend and a React/TypeScript frontend, with +a single GraphQL server feeding two SPAs, a CLI, an MCP API and an n8n community node. + +### Top-level layout + +| Path | Purpose | Stack | +| --- | --- | --- | +| `cmd/` | Go binary entry points: `probod`, `prb`, `probod-bootstrap`, `acme-keygen`, 9 one-off `migrate-*` tools | Go | +| `pkg/` | All backend code — domain services, server, IAM, agent, coredata, etc. (see module map for the 33 packages) | Go | +| `internal/` | Internal Go codegen tooling (`internal/cmd/genmodels`) | Go | +| `e2e/` | End-to-end test suite — `e2e/console/` (43 files), `e2e/mcp/` (22 files), `e2e/internal/testutil` | Go | +| `apps/console` | Main compliance React 19 + Relay 19 + Vite SPA (437 TS/TSX files) | TypeScript | +| `apps/trust` | Public unauthenticated trust-center SPA (50 TS/TSX files) | TypeScript | +| `packages/` | Shared TS workspaces — `ui`, `relay`, `routes`, `helpers`, `hooks`, `i18n`, `n8n-node`, `emails`, `cookie-banner`, `prosemirror`, `eslint-config`, `tsconfig`, `vendors`, `react-lazy`, `coredata` | TypeScript | +| `contrib/claude/` | 28 authoritative AI-instructions guides indexed by `CLAUDE.md` | docs | +| `contrib/helm/` | Kubernetes Helm chart for production deployments | YAML | +| `contrib/lima/` | Lima VM sandbox provisioner for local stack-up | shell | +| `compose.yaml`, `compose.prod.yaml` | Docker Compose stacks (postgres, keycloak, pebble ACME, seaweedfs, grafana, prometheus, tempo) | YAML | +| `GNUmakefile` | Single source of truth for build, codegen, lint, test, infra, release | Make | +| `turbo.json`, `package.json` | npm workspaces + Turborepo orchestration for `apps/*` and `packages/*` | JSON | +| `cfg/` | Local config (gitignored — `cfg/dev.yaml` written by `make dev-config`) | YAML | + +### Stack summary + +| Stack | Primary language | Entry point | Build orchestrator | +| --- | --- | --- | --- | +| Backend daemon | Go (≥1.21, target Go 1.26) | `cmd/probod/main.go` → `pkg/probod/probod.go` | GNUmakefile | +| CLI | Go | `cmd/prb/main.go` → `pkg/cmd/root/root.go` (cobra) | GNUmakefile | +| Console SPA | TypeScript / React 19 / Relay 19 | `apps/console/src/main.tsx` | Turbo + Vite | +| Trust SPA | TypeScript / React 19 / Relay 19 | `apps/trust/src/main.tsx` | Turbo + Vite | +| n8n community node | TypeScript | `packages/n8n-node/nodes/Probo/Probo.node.ts` | Turbo | + +--- + +## 2. Build & Toolchain + +The `GNUmakefile` is the canonical build interface for **both** Go and TS work — see +[`contrib/claude/make.md`](../../contrib/claude/make.md). + +| Target | What it does | +| --- | --- | +| `make build` | Builds `bin/probod`, `bin/prb`, `bin/probod-bootstrap` (Go only). | +| `make build WITH_APPS=1` | Same plus frontend codegen, Relay compile, and Vite build. | +| `make generate` | Go codegen — `gqlgen` for GraphQL resolvers, `mcpgen` for MCP. | +| `make generate WITH_APPS=1` | Adds Relay compiler + n8n GraphQL query generation. | +| `make relay` | Merges per-entity `.graphql` schema files and runs `relay-compile`. | +| `make lint` | Runs `vet` + `go-fmt` + `go-fix` + `go-lint` (golangci-lint) + `npm-lint` (eslint). | +| `make test` | Go unit tests with `-race -cover -coverprofile=coverage.out`. Use `MODULE=./pkg/foo` for one package. | +| `make test-e2e` | E2E suite against a live `probod` + Docker Compose stack (requires Lima sandbox). | +| `make stack-up` / `stack-down` | Docker Compose infra (Postgres, Pebble, Keycloak, SeaweedFS, Grafana/Prometheus/Tempo). | +| `make dev-config` | Generates `cfg/dev.yaml` via `probod-bootstrap`. | +| `make sandbox-create / start / stop / ssh / delete / status` | Lima VM lifecycle (see [`contrib/claude/sandbox.md`](../../contrib/claude/sandbox.md)). | + +### Codegen surfaces (must run after touching the corresponding source) + +| Trigger file | Command | Generator | +| --- | --- | --- | +| `pkg/server/api/console/v1/graphql/*.graphql` | `go generate ./pkg/server/api/console/v1` | gqlgen | +| `pkg/server/api/connect/v1/graphql/*.graphql` | `go generate ./pkg/server/api/connect/v1` | gqlgen | +| `pkg/server/api/trust/v1/graphql/*.graphql` | `go generate ./pkg/server/api/trust/v1` | gqlgen | +| `pkg/server/api/mcp/v1/specification.yaml` | `go generate ./pkg/server/api/mcp/v1` | mcpgen | +| Any console GraphQL fragment/op edit | `make relay` | relay-compiler | +| n8n GraphQL ops in `packages/n8n-node` | Turbo build (auto) | bespoke script | + +### Required toolchain versions + +- Go ≥1.21 (Go 1.26 idioms in active use — e.g. `new(expr)` for pointer literals). +- Node.js `^24.0.0`, npm `^11.8.0` (per root `package.json` engines). +- Docker / Docker Compose for the local stack. +- Lima for the sandbox VM (required for full e2e). + +--- + +## 3. The Four-Surface API Rule + +> **Every feature must be exposed on all four interfaces and they must stay in sync: +> GraphQL ↔ MCP ↔ CLI (`prb`) ↔ n8n.** +> Source: [`contrib/claude/api-surface.md`](../../contrib/claude/api-surface.md). + +When you add or change a backend operation, update **all four** in the same PR: + +1. **GraphQL** — schema in `pkg/server/api/{console,connect,trust}/v1/graphql/*.graphql`, + resolvers in the same package. Re-run `go generate` for that package. +2. **MCP** — declare the tool in `pkg/server/api/mcp/v1/specification.yaml`, + regenerate (`go generate ./pkg/server/api/mcp/v1`), implement the resolver body in + the hand-written tool file, add type-conversion helpers in `pkg/server/api/mcp/v1/types/*.go` + (one file per entity). Use `MustAuthorize` (panicking variant) — see + [`contrib/claude/mcp.md`](../../contrib/claude/mcp.md). +3. **CLI (`prb`)** — add a leaf command under `pkg/cmd//.go` per + [`contrib/claude/cli.md`](../../contrib/claude/cli.md). Each leaf carries its own + GraphQL `const`, an unexported `*Response` struct, and a `NewCmdVerb(f)` constructor. +4. **n8n** — register the resource in **two places**: `packages/n8n-node/nodes/Probo/actions/index.ts` + (resources map) AND `Probo.node.ts` (properties array). Add per-resource files under + `actions//`. `npx n8n-node lint` must pass — see + [`contrib/claude/n8n.md`](../../contrib/claude/n8n.md). + +> **PR-mining evidence:** PR #1132 explicit reviewer comment: *"Add e2e, mcp, prb surfaces +> to cookiebanner"* — surfaces lagging behind GraphQL is a recurring blocker. + +### Error-response rules (cross-cutting) + +Allowed user-facing error categories — only these may surface a meaningful message: + +- `NotFound`, `Forbidden`, `Invalid`, `Conflict`, `Unauthenticated`. + +Everything else **must** become a generic internal error: + +- GraphQL: `gqlutils.Internal(ctx)`. +- HTTP / MCP: `jsonutil.RenderInternalServerError(w)`. +- Resolver `switch` statements **must always include a `default:` branch** that + returns the internal error. Stack traces, SQL errors, file paths, and provider error + descriptions (OAuth `error_description`, SAML, OIDC) **must never** reach the wire. + +--- + +## 4. Configuration Propagation + +When you add, rename, or remove a config field, **all 11 of these files** change in lockstep — +see [`contrib/claude/config.md`](../../contrib/claude/config.md): -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Sections wrapped in are preserved during refresh. +1. `pkg/probodconfig/
.go` (struct definition — source of truth). +2. `pkg/probodconfig/config.go` (top-level `Config` struct wiring). +3. `pkg/probod/builder.go` (consumer wiring). +4. `GNUmakefile` (`make dev-config` arguments and `cmd/probod-bootstrap` flags). +5. `e2e/internal/testutil/testutil.go` (e2e config fixture). +6. `contrib/lima/provision.sh` (sandbox VM environment). +7. `contrib/helm/charts/probo/values.yaml`. +8. `contrib/helm/charts/probo/values-production.yaml.example`. +9. `contrib/helm/charts/probo/templates/deployment.yaml`. +10. `contrib/helm/charts/probo/templates/secret.yaml` (for any secret field, referenced via `secretKeyRef`). +11. `contrib/helm/charts/probo/templates/configmap.yaml` if non-secret. -## Project Overview +Env-var convention: **`SECTION_FIELD_NAME`** (uppercase snake-case) — e.g. `AUTH_COOKIE_DOMAIN`, +`CUSTOM_DOMAINS_RENEWAL_INTERVAL`. `cfg/dev.yaml` is gitignored; never check it in. -Probo is an open-source compliance platform (SOC-2 and related frameworks). The monorepo contains a Go backend and a TypeScript frontend, built as a single deployable binary (`bin/probod`) that serves both the API and the compiled frontend assets. +--- + +## 5. Git & Workflow -### Repository layout +> Source: [`contrib/claude/commit.md`](../../contrib/claude/commit.md), +> [`contrib/claude/release.md`](../../contrib/claude/release.md), `phase2-git-workflow.json`. -| Stack | Language | Directory | Purpose | -|---|---|---|---| -| go-backend | Go 1.26 | `pkg/`, `cmd/`, `e2e/` | API server (GraphQL, MCP), CLI (`prb`), business logic, data access, IAM, background workers | -| typescript-frontend | TypeScript (React 19) | `apps/`, `packages/` | Console admin SPA (`apps/console`), public trust center SPA (`apps/trust`), shared UI and utility packages | +### Branch naming -### Key directories +`{author}/{short-description}` — kebab-case, no ticket prefix required. Real examples from history: -- `pkg/probo/` -- core business logic and domain services -- `pkg/coredata/` -- all raw SQL and data access (the only place SQL lives) -- `pkg/server/api/` -- GraphQL APIs (console/v1, connect/v1, trust/v1) and MCP API (mcp/v1) -- `pkg/cmd/` -- CLI commands for the `prb` binary -- `apps/console/` -- admin dashboard (React + Relay + Vite, port 5173) -- `apps/trust/` -- public trust center (React + Relay + Vite, port 5174) -- `packages/` -- shared TypeScript packages (ui, relay, helpers, hooks, eslint-config, etc.) -- `e2e/console/` -- end-to-end tests (Go, run against live `bin/probod`) -- `contrib/claude/` -- detailed subsystem reference guides (15 files) - -### Binaries - -- `bin/probod` -- main server; embeds compiled frontend assets from `apps/console/dist/` and `apps/trust/dist/` -- `bin/prb` -- CLI client -- `bin/probod-bootstrap` -- bootstrap utility - -## Git & Workflow +- `aureliensibiril/model-registry` +- `aureliensibiril/checkpoint-agents` +- `SachaProbo/approval-workflow` +- `SachaProbo/mcp-error-handling` +- `bryan/eng-135-scim-does-not-work-with-ms365` (ticket prefix in the description portion is allowed but not required) ### Default branch -`main`. This is the only long-lived branch. +`main`. The history is **linear** (zero merge commits in the last 30+ merges). -### Branching strategy +### Commit message format — **free-form, NOT Conventional Commits** -Feature branches with the naming convention `/` or `/eng--`. Examples from the actual repository: +Apply the seven rules of a great commit: -- `SachaProbo/approval-workflow` -- `gearnode/graphql-dataloaders` -- `bryan/eng-136-create-access-review-api` -- `gearnode/magic-link-expiry-error` - -### Commit format +1. Subject line ≤ 50 characters, capitalized, **imperative mood**, no trailing period. +2. Subject completes the sentence *"If applied, this commit will …"*. +3. Body wraps at 72 chars, separated from subject by a blank line. Explains **what** and **why** (not how). +4. **Never** use Conventional Commits prefixes (`feat:`, `fix:`, `chore:` are wrong here). +5. **Never** include `Co-Authored-By` for AI assistants. The author is the human shipping the change. +6. **Every commit signed twice**: `git commit -s -S` — `-s` adds the DCO `Signed-off-by` trailer, `-S` adds a GPG/SSH cryptographic signature. Both are mandatory. +7. No ticket reference is required in the subject; bodies may reference IDs when relevant. -Free-form imperative mood. Not conventional commits. Titles follow "Verb Noun" pattern. All commits must be signed with `-s` (DCO Signed-off-by) and `-S` (GPG/SSH signature). The commit author must be the human, not an AI assistant. Never add `Co-Authored-By` trailers crediting bots. +Real examples from history: -Rules from `contrib/claude/commit.md`: -1. Separate subject from body with a blank line -2. Limit the subject line to 50 characters -3. Capitalize the subject line -4. Do not end the subject line with a period -5. Use the imperative mood in the subject line -6. Wrap the body at 72 characters -7. Use the body to explain what and why, not how +``` +Display cookie pattern source badge in category table +Add excluded flag to cookie pattern model +Fix maxAgeSeconds type handling in cookie pattern factory +Treat OIDC and magic link sessions as password-equivalent when assuming an org +Release v0.178.0 +``` -Examples of actual commits: -- `Add ability to disconnect Slack channel from compliance page` -- `Fix goreleaser snapshot signing creating missing bundle file` -- `Add reusable agent guardrails for prompt injection and data leaks` -- `Rename TruffleHog exclude paths file to plain text` -- `Release v0.154.2` +### Merge strategy — **rebase only** -### Merge strategy +GitHub repo settings: `allow_squash_merge=false`, `allow_merge_commit=false`, +`allow_rebase_merge=true`. **Never** create merge commits and **never** squash. Rebase your branch +onto `main` and push; CI gates listed below must be green before rebase-merge. -Rebase. The history on `main` is fully linear -- no merge commits. PRs may contain multiple commits that land individually on `main`. +> **User memory note:** small follow-up fixes (rename, typo, doc tweak) on a still-open branch +> should be folded into the previous commit via `git commit --amend` + `git push --force-with-lease`, +> not added as a separate commit. ### PR process -- Platform: GitHub (`getprobo/probo`) -- No PR template file exists -- Review before merge is required (inferred from `reviewDecision=APPROVED` on merged PRs) -- CI checks must pass before merge (see CI/CD Pipeline section below) -- Small team with high-trust review culture (3 active reviewers: gearnode, SachaProbo, codenem) - -### Release process - -- Tag format: `v0.MINOR.PATCH` (project is in 0.x series -- never bump MAJOR) -- Frequency: multiple releases per week, sometimes multiple per day -- Release commit message: exactly `Release v` -- Tag must be annotated: `git tag -a v -m "v"` -- Push both commit and tag: `git push origin main --follow-tags` -- Changelog maintained in `CHANGELOG.md` using Keep-a-Changelog categories (Added, Changed, Fixed, Removed) -- VERSION is tracked in `GNUmakefile` (currently `0.154.2`) -- CI handles binaries, Docker images, npm packages, and attestations on tag push -- Full process documented in `contrib/claude/release.md` - -## CI/CD Pipeline - -All CI is defined in GitHub Actions workflows under `.github/workflows/`. - -### On every PR and push to main (`.github/workflows/make.yaml`) - -| Job | What it does | -|---|---| -| `release-snapshot` | GoReleaser snapshot build + Cosign signing + Trivy image scan (CRITICAL/HIGH fail PR builds) + SBOM generation (Syft/Anchore) + vulnerability scan | -| `build` | `make build` (compiles Go binaries + frontend apps) + uploads artifacts | -| `lint` | `go vet` + `gofmt` + `go fix` + `golangci-lint` (via reviewdog on PRs) + `eslint` on `apps/console`, `apps/trust`, `packages/ui`, `packages/eslint-config` (via reviewdog on PRs) + `n8n-node lint` | -| `test` | `make test` with `-race -cover -coverprofile=coverage.out` + JUnit report + coverage HTML | -| `test-e2e` | Full Docker Compose stack + `make test-e2e` against live `bin/probod` | - -### On tag push (`.github/workflows/release.yaml`) +- Platform: GitHub (`getprobo/probo`). +- No `.github/PULL_REQUEST_TEMPLATE.md` exists — PR descriptions are free-form. +- CI gates (see § 7) must pass before rebase-merge. +- Formal human review is not always blocking — `cubic-dev-ai` and `github-advanced-security` bots produce + most of the comment volume; only ~21% of recent PRs have human inline review. +- The PR-mining sample identifies **3 senior reviewers** whose comments are de-facto enforced rules + (see § 13). -- GoReleaser builds `probod`, `probod-bootstrap`, `prb` binaries and multi-arch Docker images -- Docker images published to `ghcr.io/getprobo/probo` -- Cosign signatures for binaries and Docker images -- SBOM attestations and build provenance attestations -- npm package `@probo/n8n-nodes-probo` published to npmjs.org +### Releases -### Security scanning +- Tag format: **`v0.MINOR.PATCH`** — annotated tags (`git tag -a v -m 'v'`). +- Release commit message must be exactly: **`Release v`**. +- Project is in `0.x` — never bump MAJOR; bug-only releases bump PATCH, new features bump MINOR. +- Push with `git push --follow-tags`; CI on `v*` tags drives GoReleaser, multi-arch Docker images + (`ghcr.io/getprobo/probo`), the `@probo/n8n-nodes-probo` npm package, SBOMs (CycloneDX), Cosign + signatures, and SLSA attestations. +- Cadence: multiple releases per week. -- **TruffleHog** secret scanning on every push/PR (`.github/workflows/secrets.yaml`) -- **CodeQL** static analysis for Go, TypeScript/JavaScript, and GitHub Actions on every PR + weekly schedule (`.github/workflows/codeql.yml`) -- **Trivy** Docker image scanning for CRITICAL and HIGH vulnerabilities +### Changelog -### Key CI facts +`CHANGELOG.md` follows **Keep-a-Changelog** sections: `Added`, `Changed`, `Fixed`, `Removed`. +Skip non-user-facing commits (style, CI-only, internal refactors, docs-only, the release commit itself). -- Go version in CI: `1.26.1` (pinned in workflow) -- Node version: read from `.nvmrc` file -- npm version: `11.8.0` (pinned) -- Test runner: `gotestsum` (wrapping `go test`) -- Lint results posted as PR review comments via reviewdog (both golangci-lint and eslint) +--- -## Monorepo Tooling +## 6. License Headers — ISC on Every Source File -### Build orchestration +> Source: [`contrib/claude/license.md`](../../contrib/claude/license.md). -The monorepo uses **two build systems** side by side: +Every source file (`.go`, `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.sql`, `.graphql`) **must** start +with the ISC license header. Use the current year for new files; **expand to a year range when +editing an existing file** (never overwrite the original year). -1. **GNUmakefile** (root) -- orchestrates the full build including both stacks. This is the primary entry point. Key targets: `make build`, `make test`, `make lint`, `make generate`, `make test-e2e`, `make stack-up`/`stack-down`. +Comment prefix per file type: -2. **Turborepo** (`turbo.json` + root `package.json`) -- orchestrates TypeScript workspace tasks (build, dev, lint, check, relay). Tasks respect the dependency graph between `apps/*` and `packages/*`. +| Extension(s) | Comment style | +| --- | --- | +| `.go`, `.ts`, `.tsx`, `.js`, `.jsx` | `//` line comments | +| `.css` | `/* … */` block | +| `.sql` | `--` line comments | +| `.graphql` | `#` line comments | -### TypeScript workspace +Canonical text (substitute year and `// ` prefix for the relevant comment style): -npm workspaces defined in root `package.json`: ``` -"workspaces": ["apps/*", "packages/*"] +Copyright (c) 2025-2026 Probo Inc . + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ``` -Turbo tasks: `build`, `dev`, `lint`, `check`, `relay`. - -Key packages consumed across apps: -- `@probo/ui` -- shared component library (Button, Dialog, Table, Icons, Layout) -- `@probo/relay` -- Relay `FetchFunction` factory and typed GraphQL error classes -- `@probo/helpers` -- domain formatters, enum label/variant mappers, utility functions -- `@probo/hooks` -- shared React hooks -- `@probo/eslint-config` -- shared ESLint configuration -- `@probo/routes` -- routing helpers (`loaderFromQueryLoader`, `withQueryRef`) -- `@probo/react-lazy` -- lazy loading utility -- `@probo/i18n` -- internationalization -- `@probo/tsconfig` -- shared TypeScript configuration - -### Go module - -Single Go module: `go.probo.inc/probo` (defined in `go.mod`). No Go workspace or multi-module setup. - -### Code generation - -Code generation is triggered by `go generate` and `npm run relay`. The `make generate` target runs all of them: - -- **GraphQL (gqlgen)**: `go generate ./pkg/server/api/console/v1`, `go generate ./pkg/server/api/connect/v1`, `go generate ./pkg/server/api/trust/v1` -- **MCP (mcpgen)**: `go generate ./pkg/server/api/mcp/v1` -- **Relay compiler**: `npm --workspace @probo/console run relay`, `npm --workspace @probo/trust run relay` - -Generated files that must never be edited by hand: -- `pkg/server/api/*/schema/schema.go` -- `pkg/server/api/*/types/types.go` -- `pkg/server/api/mcp/v1/server/server.go` -- `apps/*/src/__generated__/` (Relay artifacts) - -### Frontend-to-backend build dependency - -The Go binary `bin/probod` embeds the compiled frontend assets. The Makefile declares explicit dependencies: `bin/probod` depends on `apps/console/dist/index.html` and `apps/trust/dist/index.html`. Use `SKIP_APPS=1 make build` to skip frontend compilation for backend-only work. - -## Cross-Stack Contracts - -The GraphQL schema files are the primary contract between the Go backend and TypeScript frontend. There are no Protobuf, OpenAPI, or Swagger specs. - -### GraphQL schemas (Go-authored, TypeScript-consumed) - -| Schema file | Go API package | Frontend consumer | -|---|---|---| -| `pkg/server/api/console/v1/schema.graphql` | `pkg/server/api/console/v1/` | `apps/console` (Relay "core" project) | -| `pkg/server/api/connect/v1/schema.graphql` | `pkg/server/api/connect/v1/` | `apps/console` (Relay "iam" project) | -| `pkg/server/api/trust/v1/schema.graphql` | `pkg/server/api/trust/v1/` | `apps/trust` | - -### How the contract flows - -1. A developer edits a `.graphql` schema file (Go side) -2. `go generate` regenerates the Go resolver stubs and type structs (gqlgen) -3. The developer implements resolver bodies in Go -4. `npm run relay` (Relay compiler) reads the same `.graphql` file and regenerates TypeScript types under `apps/*/src/__generated__/` -5. Frontend components consume the generated types via Relay fragments +The header is enforced by convention — there is no automated linter for it; reviewers spot missing +headers, and `make lint` does not currently fail on absence. Treat addition as part of the PR. -The Relay compiler config files (`apps/console/relay.config.json`, `apps/trust/relay.config.json`) point directly at the Go-side schema files using relative paths (e.g., `"schema": "../../pkg/server/api/trust/v1/schema.graphql"`). +--- -### MCP specification +## 7. CI / Quality Gates -The MCP API is defined in `pkg/server/api/mcp/v1/specification.yaml` (hand-written YAML). This is consumed only by Go code generation (`mcpgen`), not by the TypeScript frontend. +> Source: `.github/workflows/make.yaml`, `.github/workflows/release.yaml`, +> [`contrib/claude/make.md`](../../contrib/claude/make.md). -### Custom scalar type mapping +CI runs on every push and pull request to `main`: -Both stacks agree on the same custom GraphQL scalars. The Relay compiler maps them to TypeScript types: +| Job | What it enforces | +| --- | --- | +| `build` | Cross-compiles Go binaries (linux/darwin/windows/freebsd/openbsd × amd64/arm64). | +| `lint-go` | `gofmt`, `go fix`, `golangci-lint` — annotations posted to PR via **reviewdog**. | +| `lint-js` | `eslint` (via reviewdog) on `apps/console`, `apps/trust`, `packages/ui`, `packages/eslint-config`. `npx n8n-node lint` on `packages/n8n-node`. | +| `test` | Go unit tests with `-race -cover -coverprofile=coverage.out`. | +| `test-e2e` | Spins the Docker Compose stack, runs `probod`, executes `e2e/console` + `e2e/mcp` with `CGO_ENABLED=1 -race -cover`. | -| GraphQL scalar | Go type | TypeScript type | -|---|---|---| -| `GID` | `gid.GID` (base64url-encoded 192-bit ID) | `string` | -| `Datetime` | `time.Time` | `string` | -| `CursorKey` | custom cursor type | `string` | -| `Duration` | `time.Duration` | `string` | -| `BigInt` | `int64` | `number` | -| `EmailAddr` | `string` (validated) | `string` | +On `v*` tag push (release pipeline): -### GID (Global ID) format +- GoReleaser builds + signs the binary archives. +- Multi-arch Docker images published to `ghcr.io/getprobo/probo`. +- `@probo/n8n-nodes-probo` published to npm. +- **Trivy** scans Docker images — `CRITICAL` or `HIGH` findings fail the build. +- **CycloneDX SBOMs** generated via `anchore/sbom-action`. +- **Cosign** signs the artifacts; SLSA attestations are emitted. -Every entity in the system is identified by a `gid.GID` -- a 192-bit value encoding tenant ID (8 bytes), entity type (2 bytes), timestamp (8 bytes), and random uniqueness (6 bytes). Serialized as base64url (no padding) across all boundaries: database, GraphQL, JSON, and CLI. Defined in `pkg/gid/gid.go`. Entity type constants registered in `pkg/coredata/entity_type_reg.go` -- removed types are tombstoned with blank identifiers and must never be reused. +> No `.golangci.yml`, no root `eslint.config.*`, and no root `tsconfig.json` exist; lint configs +> live per-workspace and inside the CI action. There is no `.editorconfig` / `.prettierrc`. If you +> need to know the exact lint rule, read the workflow file or the workspace's local config. -### GraphQL error code contract +--- -The Go backend returns GraphQL errors with `extensions.code` values. The TypeScript `@probo/relay` package (`packages/relay/src/errors.ts`) maps these codes to typed error classes that frontend error boundaries catch: +## 8. Logging Principles (Cross-Stack) -- `UNAUTHENTICATED` -> `UnAuthenticatedError` -- `FORBIDDEN` -> `ForbiddenError` -- `INTERNAL_SERVER_ERROR` -> `InternalServerError` -- `ASSUMPTION_REQUIRED` -> `AssumptionRequiredError` -- `NDA_SIGNATURE_REQUIRED` -> `NDASignatureRequiredError` -- `FULL_NAME_REQUIRED` -> `FullNameRequiredError` +> Source: [`contrib/claude/logging.md`](../../contrib/claude/logging.md). -### Three-interface API surface rule +Both stacks log to the same observability pipeline (Loki + Tempo + Prometheus via Grafana). The +**PII-free rule applies everywhere**: -Every feature must be exposed through all three interfaces and kept in sync: +- **Never log**: emails, names, phone numbers, end-user IP addresses, postal addresses, DOBs, + passwords, tokens, signing secrets, private keys, raw HTTP bodies, full query strings (they may + contain `code`, `token`, `state` params), or external error descriptions verbatim (OAuth/OIDC/SAML + `error_description` may carry PII — sanitize to the error code only). +- **Log instead**: entity GIDs, organization IDs, error codes, durations, sanitized status names. -1. **GraphQL** -- `pkg/server/api/console/v1/schema.graphql` (+ codegen) -2. **MCP** -- `pkg/server/api/mcp/v1/specification.yaml` (+ codegen) -3. **CLI** -- `pkg/cmd/` +### Stack-specific implementations -If you add a mutation in GraphQL, add the corresponding MCP tool and CLI command. If you rename or change a type, update it everywhere. Every new Go API endpoint must have end-to-end tests in `e2e/`. +- **Go** — use `go.gearno.de/kit/log` exclusively. Always call the `*Ctx` variants + (`InfoCtx`, `WarnCtx`, `ErrorCtx`) so trace context propagates. Use typed field helpers + (`log.String`, `log.Int`, `log.Error`, `log.Duration`) — never `fmt.Sprintf` into the message. + Derive child loggers with `.Named()` at service boundaries and `.With()` for per-scope IDs. + Keep messages **static**; dynamic data goes in fields. +- **TypeScript** — `console.*` is acceptable for the SPAs (browser console). Apply the same PII + rules; redact tokens in dev tools output. Network errors should surface user-friendly messages + via the toast/notification system, not raw server responses. -## Deployment +--- -### Infrastructure +## 9. Tenant Isolation — Cross-Stack Architectural Principle -- **Database**: PostgreSQL 18.1 -- **Object storage**: SeaweedFS (S3-compatible) -- **Docker image**: `ghcr.io/getprobo/probo` (multi-arch, published on tag push) -- **Binaries**: `probod`, `prb`, `probod-bootstrap` (built by GoReleaser) +> Source: [`contrib/claude/coredata.md`](../../contrib/claude/coredata.md), +> [`contrib/claude/gid.md`](../../contrib/claude/gid.md). -### Local development stack +Probo is multi-tenant. The isolation invariant is enforced at the **data layer**, not at the API +or UI layer: -`compose.yaml` at the repository root defines the local development infrastructure: +- The `tenant_id` column exists in **every** table but is **never** stored on a Go struct + (the **single exception is `Organization`**, which represents the tenant itself). +- `tenant_id` is injected at **query time** by the `Scoper` interface implemented by + `pkg/coredata`. Every SELECT, UPDATE, DELETE goes through a Scoper that adds the predicate. +- **Every** entity carries a denormalized `organization_id` column — used by + `AuthorizationAttributes()` for IAM checks without requiring JOINs (perf-critical). +- The **frontend trusts API responses**: the SPA does no tenant filtering of its own. If the API + returns it, the user is authorized to see it. UI must never silently filter rows by tenant. +- The CLI (`prb`) selects the active organization via a config file managed by `pkg/cli/config`; + the daemon enforces the actual scoping. -| Service | Purpose | Port | -|---|---|---| -| `postgres` | Primary database | 5432 | -| `seaweedfs` | S3-compatible object storage | 8333 | -| `grafana` | Dashboards | 3001 | -| `prometheus` | Metrics collection | 9191 | -| `loki` | Log aggregation | 3100 | -| `tempo` | Distributed tracing (OTLP) | 4317 | -| `mailpit` | Email testing (SMTP + web UI) | 1025/8025 | -| `chrome` | Headless browser (PDF generation) | 9222 | -| `pebble` | ACME testing (Let's Encrypt simulator) | 14000 | -| `keycloak` | OIDC/SAML identity provider for testing | 8082 | +**Rule for new tables / queries**: add `tenant_id BYTEA NOT NULL` and `organization_id BYTEA NOT NULL` +to every entity table, and route every read/write through a `Scoper`. `coredata.NewNoScope()` is a +deliberate escape hatch only for system-level operations and is reviewed (PR #957: +*"remove use coredata.NewNoScope() where needed"*). -Start with `make stack-up`, stop with `make stack-down`. +--- -### Sandbox development +## 10. Global Identifiers (GID) — Crossing the Go ↔ TS Boundary -For full-stack development and e2e testing, a Lima VM sandbox is available via `contrib/lima/sandbox.sh`. Commands: `make sandbox-create`, `make sandbox-start`, `make sandbox-stop`, `make sandbox-delete`, `make sandbox-ssh`. Code changes are reflected immediately via virtiofs mount. Developer-specific secrets go in `.sandbox.env` (gitignored). +> Source: [`contrib/claude/gid.md`](../../contrib/claude/gid.md), `pkg/gid/gid.go`, +> `pkg/coredata/entity_type_reg.go`. -### Security artifacts +GIDs are the universal entity identifier — they replace UUIDs end-to-end. -- Cosign signatures for all binaries and Docker images -- SBOM (CycloneDX JSON) generated by Syft/Anchore on every build -- Build provenance attestations -- License compliance scanning via Trivy +### Layout (24 bytes) -## Observability +| Bytes | Content | +| --- | --- | +| `[0-7]` | Tenant ID (8 bytes) | +| `[8-9]` | Entity type (`uint16`) | +| `[10-17]` | Timestamp ms (`int64`) | +| `[18-23]` | Random data (6 bytes) | -Observability tooling exists primarily on the Go backend side. The TypeScript frontend does not have its own logging, metrics, or tracing infrastructure. +### Wire format -### Tracing (Go backend only) +- **Always base64url** when crossing process boundaries — GraphQL `ID`, MCP tool args, REST URL params, + CLI flags, n8n params, browser URLs. +- Implements `sql.Scanner`, `driver.Valuer`, `MarshalText`/`UnmarshalText` so PostgreSQL and JSON + serialize transparently. -- OpenTelemetry (`go.opentelemetry.io/otel`) is the tracing framework -- GraphQL resolvers are traced via `pkg/server/gqlutils/tracing.go` -- Traces are exported to Grafana Tempo (OTLP on port 4317 in dev) -- Used across: server, IAM, SCIM, LLM/agent, HTML-to-PDF, and SAML subsystems +### Adding a new entity type — checklist -### Logging (Go backend only) +1. Add the next sequential `uint16` constant to `pkg/coredata/entity_type_reg.go`. + **Never reuse a removed number** — leave a `_` placeholder with a comment explaining the gap. +2. Add a `case` for the new type in `gid.NewEntityFromID` (so deserialization works). +3. Call `gid.New(tenantID, FooEntityType)` in the **service-layer** `Create` method, + **not** inside `coredata` `Insert` (architecture decision in + [`contrib/claude/gid.md`](../../contrib/claude/gid.md)). +4. The frontend Relay types pick up the new ID type via the regenerated GraphQL schema — + run `make relay`. -- Framework: `go.gearno.de/kit/log` -- structured, context-aware, typed fields -- Style: named loggers with `log.String()`, `log.Int()`, etc. field constructors -- Rule: never log PII, PHI, or sensitive data (emails, names, passwords, tokens, health records). Log opaque identifiers (IDs, request IDs) instead. +### Validation -### Metrics +The `pkg/validator` framework provides `GID(entityTypes ...EntityType)` for validating that a +caller-supplied ID is a well-formed GID and (optionally) of the expected type — see +[`contrib/claude/validation.md`](../../contrib/claude/validation.md). -- Prometheus is included in the dev compose stack (port 9191) -- Grafana for dashboards (port 3001) -- Loki for log aggregation (port 3100) +--- -### Frontend observability +## 11. Error-Handling Principles -The TypeScript frontend has no logging, metrics, or tracing libraries. Errors surface to users via Relay typed error classes and React error boundaries. This is a deliberate architectural choice, not a gap to fill -- frontend observability details belong in per-stack guidelines if they evolve. +Cross-stack rules; stack-specific syntax in the per-stack guidelines. -## License +### Wrap, don't return bare -Every source file must start with the ISC license header. This applies to all file types across both stacks: `.go`, `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.sql`, `.graphql`. +- **PR-mining evidence (frequency 5, high confidence):** PR #720, #957, #1038 — bare error + returns are blocked in review (*"Missing error wrapping"*, *"error wrap"*). The Go style is to + wrap with `fmt.Errorf("cannot : %w", err)` per + [`contrib/claude/go-style.md`](../../contrib/claude/go-style.md). Error messages start with + `cannot`. The TS frontend should similarly preserve the original error via `cause:` rather than + swallowing. -Rules: -- Use the current year for new files -- When editing an existing file with a different year, update to a range (e.g., `2025-2026`) -- Never overwrite the original year -- preserve it in the range -- Comment style varies by file type (`//` for Go/TS/JS, `/* */` for CSS, `--` for SQL, `#` for GraphQL) +### Validate at the edge, propagate field-level errors -Full header templates in `contrib/claude/license.md`. +- The Go `pkg/validator` API accumulates field-level errors and propagates them as a typed + `ValidationErrors` through the `error` interface — see + [`contrib/claude/validation.md`](../../contrib/claude/validation.md). Service methods always check + `validator.Error()` before doing work. +- GraphQL exposes them as the `Invalid` user-facing category; Relay surfaces them as form errors. -## Review-Enforced Standards +### Never expose internal errors to clients -These patterns are enforced in code review across both stacks. Stack-specific patterns (Go error wrapping, SQL column naming, TypeScript hook deprecations) are documented in their respective per-stack guidelines. +- Always map unrecognized errors to `gqlutils.Internal(ctx)` (GraphQL), + `jsonutil.RenderInternalServerError(w)` (HTTP/MCP). +- Untrusted strings from external IdPs (OAuth `error_description`, SAML status messages) **must + never** appear in user-facing responses or logs. -### Cross-cutting patterns +### Error-type assertions -- **Three-interface sync rule** -- every feature must be exposed through GraphQL, MCP, and CLI. Adding a mutation in one without updating the others is an approval blocker. (enforced in code review; documented in `CLAUDE.md`) +- Use the generic `errors.AsType[T](err)` from the kit, **not** `errors.As(err, &ptr)` + (PR #1038 reviewer comment, codified in + [`contrib/claude/go-style.md`](../../contrib/claude/go-style.md)). -- **ISC license headers must be current** -- outdated copyright years are flagged. A bulk update PR (#930, 1572 lines) was merged specifically for this. Reviewers also flag individual files. (enforced in code review, frequency 2+) +--- -- **GraphQL types implementing Node must have a node resolver** -- declaring `implements Node` in the schema without a corresponding resolver implementation is flagged as incomplete. (enforced in code review) +## 12. Security Baseline (Cross-Stack) -- **Access control belongs in ABAC policies, not UI conditionals** -- when a resource's state (e.g., archived) affects allowed operations, enforcement must happen in the IAM policy system (`pkg/probo/policies.go`), not in frontend visibility toggles. Both stacks are affected. (enforced in code review, frequency 3; debated in PR #855) +### Outbound HTTP — SSRF protection by default -- **No panic in resolver code** -- using `panic` in GraphQL resolvers is an approval blocker. A dedicated cleanup PR (#865, 1425 lines) was merged to eliminate all instances. Exception: MCP resolvers use `MustAuthorize()` which deliberately panics, caught by middleware. (enforced in code review, frequency 2) +> Source: [`contrib/claude/httpclient.md`](../../contrib/claude/httpclient.md). -- **Simplify over-engineered logic** -- service methods with complex conditional patterns are flagged for simplification. Reviewers call out "useless complex logic" and request straightforward implementations. (enforced in code review, frequency 2) +- **Never** use `http.DefaultClient` or `&http.Client{}` directly — always + `go.gearno.de/kit/httpclient`. +- `httpclient.WithSSRFProtection()` is **mandatory** for: + - Any **customer-supplied URL** (webhooks, OAuth2 redirect URIs, SCIM endpoints, custom connectors). + - Any **third-party SaaS** host (Slack, Linear, GitHub, Anthropic, OpenAI, Bedrock). +- For tests against `httptest` loopback servers, combine + `WithSSRFProtection() + WithSSRFAllowLoopback()`. +- The only legitimate omission is when dialing an internal service you explicitly intend to reach. -### Approval blockers (will block merge) +### URL construction — no string formatting -These specific violations are called out as "CLAUDE.md violations" in review and will not be approved: +- **Go**: never `fmt.Sprintf` or `+` for URLs. Use `net/url` (`url.URL`, `url.Values`). For + application URLs, route through **`pkg/baseurl`** — PR #800 (frequency 3 reviewer enforcement): + *"use baseurl package for that"*. +- **TypeScript**: never template literals or `+` for URLs. Use `new URL(...)`, + `URLSearchParams`, and `encodeURIComponent` for path segments — + [`contrib/claude/ts-style.md`](../../contrib/claude/ts-style.md). -- Using `pgx.NamedArgs` instead of `pgx.StrictNamedArgs` -- `SQLFragment()` with conditional string building -- Error messages starting with "failed to" instead of "cannot" -- Missing `t.Parallel()` calls in e2e subtests -- Using `panic` in GraphQL resolvers -- Using `withQueryRef` in frontend routes (deprecated -- use page Loader component) -- Multiline function calls with mixed inline/expanded style +### Secrets — never plaintext, support rotation -### Review culture +> Source: [`contrib/claude/coredata.md`](../../contrib/claude/coredata.md) §"Sensitive data". -The team has a high-trust review culture with 3 active reviewers. Most PRs merge with minimal inline comments (61 human comments across 44 PRs in the last year). Many conventions are enforced by reference to documented CLAUDE.md rules rather than lengthy discussion. +- Store sensitive columns as `BYTEA`. Hash tokens with **SHA-256** (`Hashed*` fields), passwords with + **PBKDF2** (`HashedPassword`), decryptable secrets with **AES-256-GCM** (`Encrypted*` fields). +- **Signing keys / API keys must be configured as arrays** to support rotation — + PR #957 reviewer comment: *"should be an array no, so we can rotate them if needed?"*. Code with + a single fixed signing key is a review blocker. + +### PKCE / OAuth code lifecycle + +- Auth-code or PKCE flows **must clean up the auth code on failure** — leaving a stale code is a + security defect. PR #957 reviewer comment: *"Security issue, if the code challenge failed it will + not delete the code."* + +### UUIDs + +- Use `go.gearno.de/crypto/uuid`; **never** `github.com/google/uuid`. (`coredata.md` rule.) + +### Container hygiene + +- Trivy gates `CRITICAL`/`HIGH` vulnerabilities on every image build. +- Releases are signed with Cosign and ship CycloneDX SBOMs and SLSA attestations. + +--- + +## 13. Code-Review-Enforced Standards + +> Distilled from `phase2-reviews.json` — 80 human comments across 21 PRs from Feb–May 2026. +> **High-confidence patterns (frequency ≥ 3) are de-facto blockers**. + +### What reviewers consistently block + +| # | Rule | Frequency | Example | +| --- | --- | --- | --- | +| 1 | All SQL queries live in `pkg/coredata` — never inline raw SQL in `pkg/probo`, workers, or handlers | 4 (high) | PR #800 *"query should be in coredata."* | +| 2 | Wrap errors with context — bare `return err` blocks approval | 5 (high) | PR #957 *"error wrap"* | +| 3 | Remove useless / redundant comments that restate the code | 4 (high) | PR #957 *"remove useless comment"* | +| 4 | **GraphQL fields whose resolvers can fail must NOT be non-null (`!`)** — use Relay `@required` to handle null | 4 (high) | PR #720 *"cannot be bang there as resolver can fail. We have to use @require in relay for type issue."* | +| 5 | Inline SVGs are forbidden — extract as React components or use Phosphor icons | 4 (high) | PR #957 *"all SVGs should be in a react component"* | +| 6 | Frontend must use **Relay-generated types** — never declare local TS types that duplicate GraphQL output | 4 (high) | PR #800 *"We don't declare local types anymore, we should use relay generated types directly"* | +| 7 | Use `pkg/baseurl` for URL construction in Go | 3 (med) | PR #800 *"use baseurl package for that"* | +| 8 | Constructor naming: prefix with `New*` (not `Build*`, `Make*`) | 3 (med) | PR #957 *"s/BuildMetadata/NewMetadata/g"* | +| 9 | Don't add `json` struct tags to structs that aren't serialized to external output | 3 (med) | PR #1023 *"i would avoid json tag at this level"* | +| 10 | Mutations should update the Relay store; avoid full refetch when the mutation response carries the data | 3 (med) | PR #1000 *"Why should we refetch instead of just updating the store?"* | +| 11 | Security-sensitive packages (OAuth2, OIDC, PKCE, ID-token) require **100% unit test coverage** | 3 (med) | PR #957 *"this file must have unit test 100%"* | +| 12 | New features need e2e tests in `e2e/` | 2 (med) | PR #1102 *"Maybe add some e2e tests?"* | +| 13 | Webhook / external-API payloads must NOT use `coredata` structs directly — define a separate public-API DTO | 2 (med) | PR #720 *"We can't use coredata object as payload for webhook. We must consider webhook payload as public API."* | +| 14 | Tests live in their own package (`*_test` black-box), not inside the package under test | 2 (med) | PR #1023 *"this test must no be in probo package."* | +| 15 | `commit*` is **not** a good name for a React mutation handler — use the action verb | 2 (med) | PR #1073 *"not fan of `commit*` naming."* | +| 16 | Prefer `@probo/ui` shared primitives over duplicating UI in app pages | 2 (med) | PR #957 *"nothing to reuse from @probo/ui here instead?"* | +| 17 | Extract complex `switch`/`case` blocks into private dedicated functions | 2 (med) | PR #957 *"switch case to private dedicated function."* | +| 18 | Use `http.StatusXxx` constants, not bare integer status codes | 1 (low) | PR #720 *"Use http.StatusX const please."* | +| 19 | Avoid JOINs in coredata when two queries are clearer | 1 (low) | PR #720 *"i will not do join here. I would rather just load the event…"* | + +### Reviewer hot zones (most-commented paths) + +- `pkg/iam/oauth2server/` — OAuth2/OIDC code is reviewed line-by-line. +- `pkg/server/api/console/v1/schema.graphql` — nullability and `extend type` use are scrutinized. +- `pkg/agent/checkpoint.go`, `pkg/llm/message.go` — naming and json-tag minimalism. +- `pkg/webhook/sender.go` — public-API contract concerns. +- `apps/console/src/pages/` — Relay store updates, type origin, icon source. + +--- + +## 14. Known Drift / Active Violations + +These are concrete inconsistencies between the documented rules and the code as of 2026-05-01. +Reviewers should call them out when they appear; new code must follow the documented rule. + +- **`pkg/probo/agent_run.go:472`** — hardcoded SQL literal in service code, violating the + *"all SQL in `pkg/coredata`"* rule (§ 13 #1). When touching this file, migrate the query + into a coredata method. +- **`pkg/server/api/csp.go`** — outbound HTTP path lacks the `WithSSRFProtection()` netcheck guard + expected by [`contrib/claude/httpclient.md`](../../contrib/claude/httpclient.md). +- **OIDC `error_description` PII leak** — at least one path surfaces the raw provider + `error_description` to clients/logs, contrary to § 8 and § 11. Sanitize to the error code only. +- **`contrib/claude/app-arborescence.md`** explicitly notes that the codebase does not yet + fully match the *"`pages/` IS the route tree"* layout. New work must match the doc; legacy + code in `apps/console/src/routes/` is being migrated incrementally. +- **`contrib/claude/react-components.md`** similarly notes that older components do not match the + *"props for configuration, data from hooks"* rule. Refactor opportunistically when touching a file. +- **No root-level `.golangci.yml`, `eslint.config.*`, `tsconfig.json`, `.editorconfig`, or + `.prettierrc`** — lint and format rules live per-workspace and inside the CI action. Documented + rules in `contrib/claude/` are authoritative; if a tool config disagrees, the doc wins. + +--- + +## 15. Documentation Index — `CLAUDE.md` and `contrib/claude/` + +`CLAUDE.md` (a.k.a. `AGENTS.md`) at the repo root is a thin index pointing to **28 +expert-authored guides** under `contrib/claude/`. **These guides are authoritative.** This +shared.md is a synthesis; whenever the two disagree, the `contrib/claude/` doc wins, and this +file should be regenerated. + +| Topic | Doc | +| --- | --- | +| Build / GNUmakefile | [`make.md`](../../contrib/claude/make.md) | +| Four-surface API rule | [`api-surface.md`](../../contrib/claude/api-surface.md) | +| Go style | [`go-style.md`](../../contrib/claude/go-style.md) | +| TS style | [`ts-style.md`](../../contrib/claude/ts-style.md) | +| Go testing | [`go-testing.md`](../../contrib/claude/go-testing.md) | +| Go service orchestration | [`go-service.md`](../../contrib/claude/go-service.md) | +| Go worker pattern | [`go-worker.md`](../../contrib/claude/go-worker.md) | +| HTTP client / SSRF | [`httpclient.md`](../../contrib/claude/httpclient.md) | +| GIDs | [`gid.md`](../../contrib/claude/gid.md) | +| Coredata / SQL | [`coredata.md`](../../contrib/claude/coredata.md) | +| Logging | [`logging.md`](../../contrib/claude/logging.md) | +| GraphQL backend | [`graphql.md`](../../contrib/claude/graphql.md) | +| MCP backend | [`mcp.md`](../../contrib/claude/mcp.md) | +| CLI (`prb`) | [`cli.md`](../../contrib/claude/cli.md) | +| Authorization (IAM) | [`authorization.md`](../../contrib/claude/authorization.md) | +| Validation | [`validation.md`](../../contrib/claude/validation.md) | +| E2E tests | [`e2e.md`](../../contrib/claude/e2e.md) | +| Agent framework | [`agent.md`](../../contrib/claude/agent.md) | +| Frontend file layout | [`app-arborescence.md`](../../contrib/claude/app-arborescence.md) | +| Relay client | [`relay.md`](../../contrib/claude/relay.md) | +| React components | [`react-components.md`](../../contrib/claude/react-components.md) | +| @probo/ui + Tailwind | [`ui.md`](../../contrib/claude/ui.md) | +| Config propagation | [`config.md`](../../contrib/claude/config.md) | +| Commit conventions | [`commit.md`](../../contrib/claude/commit.md) | +| License headers | [`license.md`](../../contrib/claude/license.md) | +| Release process | [`release.md`](../../contrib/claude/release.md) | +| Lima sandbox | [`sandbox.md`](../../contrib/claude/sandbox.md) | +| n8n community node | [`n8n.md`](../../contrib/claude/n8n.md) | + +Per-stack synthesizers downstream produce: + +- `.claude/guidelines/go-backend/{conventions,patterns,pitfalls,testing}.md` — Go-specific rules. +- `.claude/guidelines/typescript-frontend/{conventions,patterns,pitfalls,testing}.md` — TS-specific rules. + +This file is the cross-cutting layer they all share. ## Team Notes -_Reserved for manual additions by the team -- this section is preserved on refresh._ +_Reserved for manual additions by the team — this section is preserved on refresh._ diff --git a/.claude/guidelines/typescript-frontend/conventions.md b/.claude/guidelines/typescript-frontend/conventions.md index 5564fdd066..8c5b486431 100644 --- a/.claude/guidelines/typescript-frontend/conventions.md +++ b/.claude/guidelines/typescript-frontend/conventions.md @@ -1,176 +1,263 @@ -# Probo -- TypeScript Frontend -- Conventions +# Probo — TypeScript Frontend — Conventions -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) +> For commit format, branching, license headers, GIDs, four-surface API rule, configuration +> propagation, and PII-free logging: see [shared.md](../shared.md). +> Authoritative source: [`contrib/claude/ts-style.md`](../../../contrib/claude/ts-style.md), +> [`contrib/claude/react-components.md`](../../../contrib/claude/react-components.md), +> [`contrib/claude/ui.md`](../../../contrib/claude/ui.md). -See [shared.md -- Git & Workflow](../shared.md#git--workflow) for commit format, branching, and merge strategy. -See [shared.md -- License](../shared.md#license) for ISC license header requirements on all source files. +--- -## TypeScript Configuration +## 1. File and Symbol Naming -- **Strict mode**: enabled across apps and packages via shared `@probo/tsconfig` -- **Target**: ES modules (`"type": "module"` in all `package.json` files) -- **Source-level consumption**: shared packages point `"main"` directly at `./src/index.ts` (raw TypeScript). This works with Vite but will not work in plain Node.js environments. +| Kind | Convention | Examples | +| --- | --- | --- | +| Component file (`.tsx`) | PascalCase, matches default export | `FindingsPage.tsx`, `BannerSettingsForm.tsx`, `RootErrorBoundary.tsx` | +| Page-loader file | `PageLoader.tsx` | `FindingsPageLoader.tsx`, `NewOrganizationPageLoader.tsx` | +| Skeleton | `PageSkeleton.tsx` co-located with the Page | `FindingsPageSkeleton.tsx` | +| Layout | `Layout.tsx` | `OrganizationLayout.tsx` | +| Hook | `useXxx.ts`, camelCase | `useOrganizationId.ts`, `useToggle.ts`, `usePageTitle.ts` | +| Route file | `routes.ts` (colocated) — never `Routes.tsx` | `pages/iam/organizations/people/routes.ts` | +| Generated Relay types | `__generated__/{core,iam}/.graphql.ts` | (auto) | +| Sub-component folder | `_components/` (underscore-prefixed) | `pages/organizations/findings/_components/FindingRow.tsx` | +| `tv()` variants | `variants.ts` next to the component | `packages/ui/src/atoms/Button/variants.ts` | -## Code Style +**Child-route components use `*Page` suffix, not `*Tab`.** The arborescence guide is explicit: +child routes are full pages, not tabs. Existing `*Tab.tsx` (e.g. `VendorOverviewTab.tsx`) is +legacy — rename when you touch them. -ESLint configuration is centralized in `@probo/eslint-config` (`packages/eslint-config/`). Key rules enforced: +**Path alias `#` → `src/`.** Always use `#/hooks/...`, `#/components/...`, `#/pages/...`. Never +relative `../../../`. -### Formatting (via `@stylistic/eslint-plugin`) +--- -| Rule | Setting | -|------|---------| -| Indent | 2 spaces | -| Quotes | Double quotes (`"`) | -| Semicolons | Always required | -| Trailing commas | `always-multiline` | -| Arrow parens | As-needed | -| Brace style | `1tbs` | -| Max line length | 120 characters (warn), ignores strings/templates/comments/URLs | +## 2. Component Code Style -### Import Ordering (via `eslint-plugin-import-x`) +> Source: [`contrib/claude/react-components.md`](../../../contrib/claude/react-components.md). -Three groups separated by blank lines: +```tsx +// Good — function declaration, props destructured in parameters +export function FindingRow({ finding, onDelete }: FindingRowProps) { + const data = useFragment(FindingFragment, finding); + return ...; +} + +// Bad — arrow const + React.FC + body destructure +export const FindingRow: React.FC = (props) => { + const { finding, onDelete } = props; + ... +}; +``` + +Rules: + +- **Function declarations**, not arrow `const` (legacy: `packages/ui/src/atoms/Button/Button.tsx`). +- **No `React.FC`**; type props on the parameter. +- **Destructure props in parameters**. Accept the bare `props` object only when the destructure + line would exceed 100 chars. +- **Non-callback props before callback props** in interface declarations: + ```ts + interface FindingRowProps { + finding: FindingRowFragment$key; + isSelected: boolean; + onDelete: (id: string) => void; + onSelect: (id: string) => void; + } + ``` +- Props interface name: `Props`. Use `interface`, not `type`, unless the shape + needs unions / mapped types. +- **Named exports for everything**, except *route-target* `*PageLoader` / `*Page` files which use + `export default` so `lazy(() => import(...))` resolves cleanly. + +--- + +## 3. Icons + +> PR-mining (frequency 4, high): PR #957 — *"all SVGs should be in a react component"*. + +- **Phosphor icons first.** `import { CookieIcon, TrashIcon } from "@phosphor-icons/react"`. + Import named icons, not `*`. +- **`@probo/ui` Icon set** only when Phosphor has no equivalent (audit before adding). +- **Never inline SVG markup** in a component. Extract as a React component or — preferably — pick a + Phosphor icon. +- **Never emoji** as iconography. + +--- + +## 4. Types — Use Relay-Generated, Don't Redeclare -1. **Built-in / External** packages (e.g., `react`, `react-relay`, `@probo/ui`) -2. **Aliased imports** using the `#/` prefix (e.g., `#/components/`, `#/hooks/`) -3. **Relative imports** (parent, sibling, index) +> PR-mining (frequency 4, high — highest-frequency frontend block): PR #800 *"We don't declare +> local types anymore, we should use relay generated types directly"*. -Within each group, imports are sorted alphabetically (case-insensitive). The `#/` alias resolves to `src/` within each app. +When a component reads GraphQL data, type it from the Relay-generated artifact: ```tsx -// Example from apps/trust/src/pages/DocumentPage.tsx -import { formatError } from "@probo/helpers"; -import { useSystemTheme } from "@probo/hooks"; -import { useTranslate } from "@probo/i18n"; -import { UnAuthenticatedError } from "@probo/relay"; -import { Button, IconArrowDown, Spinner, useToast } from "@probo/ui"; -import { useEffect, useState } from "react"; -import { useMutation, usePreloadedQuery } from "react-relay"; -import { Link, useNavigate } from "react-router"; -import { graphql } from "relay-runtime"; - -import { PDFPreview } from "#/components/PDFPreview"; -import { getPathPrefix } from "#/utils/pathPrefix"; - -import type { DocumentPageQuery } from "./__generated__/DocumentPageQuery.graphql"; +import type { FindingRowFragment$key, FindingRowFragment$data } from "#/__generated__/core/FindingRowFragment.graphql"; + +type Finding = FindingRowFragment$data; // do NOT redeclare ``` -### Relay ESLint Rules (via `eslint-plugin-relay`) +Use the shared helpers in `apps/console/src/types.ts`: -The `ts-recommended` ruleset is enabled. Key rules: -- `relay/must-colocate-fragment-spreads` -- fragments must be in the same file as their spread -- `relay/unused-fields` -- all fetched fields must be used +- `NodeOf` — extracts the node type from a Relay connection edges array. +- `ItemOf` — extracts the element type of any array. -Legacy `hooks/graph/` files carry `eslint-disable` comments for these rules. New code must not add such comments. +Local interfaces are appropriate only for **non-GraphQL** shapes (form state, UI-only flags, route +params). The IAM environment's types live under `__generated__/iam/`; pages under `src/pages/iam/` +must import from there, not from `__generated__/core/`. -## Naming Conventions +`AppRoute` (from `@probo/routes`) is asserted with `satisfies AppRoute[]` on every route array. -### File Naming (universal) +--- -| Type | Convention | Example | -|------|-----------|---------| -| React components, pages, layouts | PascalCase `.tsx` | `RisksPage.tsx`, `Badge.tsx`, `MainLayout.tsx` | -| Hooks | camelCase with `use` prefix | `useToggle.ts`, `useMutationWithToasts.ts` | -| Route files | camelCase with `Routes` suffix | `riskRoutes.ts`, `assetRoutes.ts` | -| Loader components | PascalCase with `Loader` suffix | `DocumentsPageLoader.tsx`, `MeasuresPageLoader.tsx` | -| Helper/utility files | camelCase | `pathPrefix.ts`, `audits.ts`, `error.ts` | -| Test files | `.test.ts` co-located | `file.test.ts`, `array.test.ts` | -| Story files | `.stories.tsx` co-located | `Button.stories.tsx` | -| Constants | `constants.ts` | `constants.ts` | +## 5. Translator Injection -### Component and Variable Naming (universal) +`@probo/i18n` exports `useTranslate()` which returns a function commonly bound to `__`: -- **Components**: PascalCase function names matching the file name -- **Hooks**: `use` prefix, camelCase (`useToggle`, `useFormWithSchema`, `useOrganizationId`) -- **Relay fragments**: name matches `{ComponentName}Fragment_{fieldName}` (e.g., `VendorContactsTabFragment_contact`) -- **Relay queries**: name matches `{ComponentName}Query` (e.g., `DocumentsPageQuery`) -- **Relay mutations**: name matches `{ComponentName}{Action}Mutation` (e.g., `DocumentPageExportDocumentMutation`) -- **Route arrays**: camelCase with `Routes` suffix (e.g., `const assetRoutes = [...]`) -- **Relay environments**: camelCase descriptive name (`coreEnvironment`, `iamEnvironment`, `consoleEnvironment`) +```tsx +const { __ } = useTranslate(); +return ; +``` -### Domain Helper Naming (module-specific: packages/helpers) +**Universal rule:** every helper that returns user-facing text **takes `__: Translator` as its +first argument**: -Each domain file follows a strict naming pattern: +```ts +// packages/helpers/src/format/formatError.ts +import type { Translator } from "@probo/i18n"; -- Enum array: `const fooStates = [...] as const` (or `fooTypes`, `fooCategories`) -- Label function: `getFooStateLabel(__: Translator, state)` -- translator is always the first argument -- Variant function: `getFooStateVariant(state)` -- returns badge variant string -- Options function: `getFooStateOptions(__)` -- returns `{ value, label }[]` for dropdowns +export function formatError(__: Translator, err: unknown): string { ... } +``` -See `packages/helpers/src/audits.ts` for the canonical example. +This includes `formatError`, date/time formatters that produce labels, and any string-builder +shared across modules. The reason: `@probo/helpers` cannot call hooks; passing `__` keeps the +helper pure and unit-testable. -## Export Patterns (universal) +> Current state: i18n is **dormant**. Loaders return `{}` and the language is hard-coded `"en"`. +> The injection pattern still applies — when translations land later, helpers are already +> compatible. -- **Named exports** everywhere. Default exports are used only for page components (required by `React.lazy`). -- **Barrel files**: `src/index.ts` re-exports all public symbols from each package. -- All public exports from `@probo/ui` go through `packages/ui/src/index.ts`. -- Icon components have their own barrel at `packages/ui/src/Atoms/Icons/index.tsx`. +--- -## Directory Structure +## 6. URL Construction -### Apps (apps/console, apps/trust) +> Source: [`contrib/claude/ts-style.md`](../../../contrib/claude/ts-style.md). Cross-stack rule — +> see also [shared.md § 12](../shared.md#12-security-baseline-cross-stack). +```ts +// Good +const url = new URL("/api/console/v1/graphql", window.location.origin); +url.searchParams.set("organizationId", organizationId); + +// Or for in-app navigation paths: +const path = `/organizations/${encodeURIComponent(organizationId)}/findings`; ``` -src/ - __generated__/ # Relay compiler output (never edit) - components/ # Shared cross-domain UI components - hooks/ # Reusable hooks - graph/ # LEGACY -- Relay queries/fragments (do not add new files) - forms/ # react-hook-form + zod schema hooks - pages/ # Route-level page components - organizations/ # Business domain pages (console) - / # One directory per domain - _components/ # Private sub-components - dialogs/ # Modal dialogs with mutation handling - tabs/ # Detail-page tab components - providers/ # React context providers - routes/ # Route definition files (console only) - utils/ # Utility functions - environments.ts # Relay environment configuration (console) - routes.tsx # Route assembly - main.tsx # App entry point - types.ts # Shared utility types (NodeOf, ItemOf) + +```ts +// Bad +const url = "/api/console/v1/graphql?organizationId=" + organizationId; +const url = `/api/console/v1/graphql?organizationId=${organizationId}`; ``` -### Shared Packages +Never template-literal-concat URLs. Use `URL`, `URLSearchParams`, and `encodeURIComponent` for any +caller-controlled path segment. + +--- + +## 7. Imports, Barrels, Module Structure -| Package | Structure | -|---------|-----------| -| `packages/ui` | `src/Atoms//`, `src/Molecules//`, `src/Layouts/` | -| `packages/helpers` | Flat `src/` -- one file per domain, all re-exported from `index.ts` | -| `packages/hooks` | Flat `src/` -- one file per hook, barrel `index.ts` | -| `packages/relay` | Flat `src/` -- `errors.ts`, `fetch.ts`, `index.ts` | -| `packages/emails` | `src/` (TSX templates), `templates/` (.txt), `scripts/` (build), `assets/` (images) | +- **`@probo/helpers` uses a barrel `index.ts`.** When you add a new helper, **update + `packages/helpers/src/index.ts`** or the symbol is invisible to callers. Same for any + `packages/*` that exports through a barrel. +- Barrel re-exports are `export * from './foo'` or `export { foo } from './foo'`. Don't introduce + a default export. +- **Import generated Relay types** with `import type { ... }` so they don't appear in the runtime + bundle. -### Relay Generated Files +--- -Generated types land in `__generated__/` subdirectories relative to the file declaring the operation: +## 8. Reusing `@probo/ui` — Don't Re-implement -- Console core API: `apps/console/src/__generated__/core/` -- Console IAM API: `apps/console/src/__generated__/iam/` -- Trust API: `apps/trust/src/**/__generated__/` (next to declaring files) +> PR-mining (frequency 2): PR #957 — *"nothing to reuse from @probo/ui here instead?"*. -These files must never be edited by hand. Regenerate with `npm run relay` or `npx relay-compiler`. +Before writing a new local component in `apps/console/src/components/` or a `_components/` folder, +search `packages/ui/src/`. The library already covers Button, Card, Field, Dialog, Dropdown, +Select, Combobox, Tabs, Badge, Avatar, Toast, Confirm, PageHeader, Table primitives, +Tooltip, Popover, Skeleton primitives, etc. -## i18n Conventions (universal) +Local components should: -All user-visible strings in components and molecules go through the `useTranslate()` hook from `@probo/i18n`: +- Compose `@probo/ui` primitives — never duplicate styles for "just a slightly different button". +- If a variant is missing in `@probo/ui`, add the variant **in `@probo/ui`** rather than fork it + in `apps/console`. + +--- + +## 9. Mutation Result Naming and Store Updates + +(Repeated here because reviewers enforce it on every PR — full detail in +[patterns.md § Mutation Handler Naming](./patterns.md#7-mutation-handler-naming).) ```tsx -const { __ } = useTranslate(); -return ; +// Good +const [createFinding, isCreating] = useMutation(CreateFindingMutation); + +// Bad +const [commit, isInFlight] = useMutation(...); ``` -Domain helper functions accept a `Translator` (`(s: string) => string`) as their first argument rather than importing `useTranslate` directly, keeping the helpers decoupled from React. +Avoid `refetch()` after a mutation — use `@appendEdge` / `@deleteEdge` / `updater`. PR-mining +(frequency 3): PR #1000. + +--- + +## 10. Branding Tokens + +| Surface | Source of truth | +| --- | --- | +| App UI (Console, Trust) | `packages/ui` Tailwind theme + tokens; consumed via `tailwindcss` v4 | +| Email templates | `packages/emails/src/EmailLayout.tsx` (color palette, fonts, header logo) | + +These two are **deliberately separate** — emails ship as static HTML rendered at build time and +must not depend on Tailwind runtime. Update both when you change brand colors. + +--- + +## 11. tsconfig — Per-Workspace, No Root + +There is **no root `tsconfig.json`**. Each workspace owns its own `tsconfig.json`, typically +extending one of the presets in `packages/tsconfig/`. + +> Known gap: **`@probo/helpers` does not enable `strict` mode.** Other packages do. Treat strict as +> the default for new packages; if `strict` is off, that is a debt to flag, not a license to relax. + +--- + +## 12. License Headers + +See [shared.md § 6](../shared.md#6-license-headersisc-on-every-source-file). Apply the +ISC header to every `.ts` / `.tsx` / `.css` / `.js` file with `//` (or `/* */` for CSS) comments, +expanding the year range when editing an existing file. + +--- + +## 13. Review-Enforced Standards (Frontend Subset) + +These come straight from PR mining (see [shared.md § 13](../shared.md#13-code-review-enforced-standards) +for the full table). They are **de-facto blockers**: -Note: the `TranslatorProvider` currently returns an empty translation map (stub). All `__()` calls fall back to the key string. This is intentional for now -- do not add new i18n infrastructure without addressing this TODO. +1. **Use Relay-generated types**, never local TS types duplicating GraphQL output (frequency 4). +2. **GraphQL fields whose resolvers can fail must use `@required`**, not non-null `!` (frequency 4). +3. **Inline SVGs are forbidden** — Phosphor icons or extracted React components only (frequency 4). +4. **Prefer `@probo/ui` primitives** over duplicated local UI (frequency 2). +5. **Mutations should update the Relay store**, not refetch when the mutation response carries the + data (frequency 3). +6. **`commit*` is not a good name** for a mutation handler — use the action verb (frequency 2). -## Review-Enforced Standards +--- -These patterns are enforced by code reviewers on TypeScript frontend PRs: +## 14. Git & Workflow -- **Do not use `withQueryRef`** -- use a Loader component pattern instead. This is an approval blocker. (enforced in code review; see [shared.md -- Approval blockers](../shared.md#review-enforced-standards)) -- **Do not use `useMutationWithToasts`** -- use `useMutation` + `useToast` separately. (enforced in code review) -- **Access control in ABAC, not UI conditionals** -- when resource state affects allowed operations, enforcement must be in the IAM policy system, not in frontend visibility toggles. (enforced in code review; see [shared.md -- Review-Enforced Standards](../shared.md#review-enforced-standards)) -- **ISC license headers must be current** -- see [shared.md -- License](../shared.md#license). +See [shared.md § 5](../shared.md#5-git--workflow) for branching (`{author}/{kebab-desc}`), +commit format (free-form, imperative, never Conventional Commits, never `Co-Authored-By` for AI), +the dual-sign requirement (`git commit -s -S`), and the rebase-only merge strategy. diff --git a/.claude/guidelines/typescript-frontend/index.md b/.claude/guidelines/typescript-frontend/index.md index 9e911f3d83..d3ed90b5ca 100644 --- a/.claude/guidelines/typescript-frontend/index.md +++ b/.claude/guidelines/typescript-frontend/index.md @@ -1,64 +1,98 @@ -# Probo -- TypeScript Frontend +# Probo — TypeScript Frontend -> Auto-generated by potion-skill-generator. Last generated: 2026-03-30 -> Cross-cutting conventions: see [shared.md](../shared.md) -> Sections wrapped in will be preserved during refresh. +> Auto-generated by potion-skill-generator. Last generated: 2026-05-01. +> Cross-cutting conventions: see [shared.md](../shared.md). +> Authoritative source-of-truth lives in [`contrib/claude/`](../../../contrib/claude/). +> Sections wrapped in `` are preserved during refresh. + +--- ## Architecture Overview -The TypeScript frontend is a monorepo workspace containing two React 19 SPAs and a collection of shared packages. The admin console (`apps/console`) and the public trust center (`apps/trust`) both use Relay 19 as their GraphQL client, React Router v7 for client-side routing with data loaders, and Tailwind CSS v4 for styling. They communicate with the Go backend through GraphQL schemas that serve as the cross-stack contract (see [shared.md -- Cross-Stack Contracts](../shared.md#cross-stack-contracts) for details). +The frontend is a **TypeScript monorepo** orchestrated by **Turborepo + npm workspaces**, with two +React 19 SPAs (`apps/console`, `apps/trust`) consuming a shared GraphQL backend through **Relay 19**. +Shared code lives in `packages/*` (component library, Relay helpers, routing primitives, hooks, +helpers, i18n, email templates, ProseMirror bridge, cookie banner web component, n8n community node, +…). Build is **Vite** for the SPAs, **esbuild** for the n8n node, **tsx** scripts for the email +templates. + +Two **independent Relay environments** coexist inside `apps/console`: `coreEnvironment` (Console API, +`/api/console/v1/graphql`) and `iamEnvironment` (Connect API, `/api/connect/v1/graphql`). They are +separated *at Vite/Babel level*: code under `apps/console/src/pages/iam/**` compiles against +`__generated__/iam/`; everything else compiles against `__generated__/core/`. Crossing this boundary +silently fails Relay codegen — see [patterns.md § Relay Data Flow](./patterns.md#relay-data-flow). -Both apps follow a feature-slice architecture where route files define the page tree, pages contain colocated Relay queries and fragments, and shared UI primitives live in `@probo/ui`. The Relay compiler reads the Go-authored `.graphql` schema files directly and generates TypeScript types into `__generated__/` directories -- there are no hand-written API types on the frontend. The compiled frontend assets are embedded into the Go binary (`bin/probod`), so `make build` must compile both apps before the Go build step. +The route system is mid-migration. Legacy routes live under `apps/console/src/routes/` and use +`loaderFromQueryLoader` / `withQueryRef` from `@probo/routes` (both **deprecated**). The target is a +**colocated `routes.ts` per page folder**, with a `*PageLoader.tsx` that mounts the Relay provider +and uses `useQueryLoader` + Suspense. New work follows the new pattern; +[`contrib/claude/app-arborescence.md`](../../../contrib/claude/app-arborescence.md) explicitly notes +the codebase has not finished migrating. -A set of shared packages (`packages/*`) provides the design system (`@probo/ui`), the Relay network layer (`@probo/relay`), domain helpers (`@probo/helpers`), shared hooks (`@probo/hooks`), routing utilities (`@probo/routes`), i18n (`@probo/i18n`), and ESLint configuration (`@probo/eslint-config`). Two additional packages serve specialized roles: `packages/emails` provides React Email templates compiled to Go template HTML, and `packages/n8n-node` is an n8n community node for the Probo API. +--- ## Module Map -| Module | Package Name | Purpose | Key Patterns | -|--------|-------------|---------|--------------| -| `apps/console` | `@probo/console` | Admin dashboard SPA (port 5173) | Two Relay environments (core + IAM), feature-slice routes, snapshot mode, permission fragments, Loader component pattern | -| `apps/trust` | `@probo/trust` | Public trust center SPA (port 5174) | Single Relay environment, path-prefix routing, auth redirects with continue URLs, NDA signing flow | -| `packages/ui` | `@probo/ui` | Shared design system | Tailwind Variants (`tv()`), Radix primitives, Atom/Molecule/Layout hierarchy, Storybook 10 | -| `packages/relay` | `@probo/relay` | Relay FetchFunction factory + typed error classes | `makeFetchQuery`, typed GraphQL error-to-exception mapping | -| `packages/helpers` | `@probo/helpers` | Domain formatters, enum labels/variants, utilities | Translator injection pattern, `as const` enum arrays, `getXLabel`/`getXVariant` | -| `packages/hooks` | `@probo/hooks` | Shared React hooks | `useToggle`, `useList`, `usePageTitle`, `useCopy`, `useSystemTheme` | -| `packages/emails` | `@probo/emails` | React Email templates for transactional emails | TSX with Go template placeholders, build to `.html.tmpl`, embedded in Go binary | -| `packages/n8n-node` | `@probo/n8n-nodes-probo` | n8n community node for Probo API | Resource/Operation slices, inline GraphQL, cursor pagination | +| Module | Path | Purpose | Key patterns | +| --- | --- | --- | --- | +| `apps-console` | `apps/console` | Main compliance SPA (437 TS/TSX files) | Two Relay envs (core/iam), `*PageLoader` + Suspense, feature-slice pages | +| `apps-trust` | `apps/trust` | Public trust portal (50 files) — magic-link / OIDC / NDA | Isolated Relay stores per auth lazy-page, public-token query var | +| `packages-ui` (`@probo/ui`) | `packages/ui` | Component library (285 files), Atoms / Molecules | `tailwind-variants` `tv()`, flat compound exports (`*Root`, `*Shell`, `*Skeleton`), custom `Slot` for `asChild` | +| `packages-relay` (`@probo/relay`) | `packages/relay` | `makeFetchQuery` + 6 typed error classes | `Object.setPrototypeOf` after `super()` so `instanceof` works through transpilation | +| `packages-routes` (`@probo/routes`) | `packages/routes` | `AppRoute` type + legacy `loaderFromQueryLoader` / `withQueryRef` | **Deprecated helpers** retained for legacy `src/routes/` callers — new code uses `*PageLoader` | +| `packages-helpers` (`@probo/helpers`) | `packages/helpers` | `formatDate`, `formatError`, `sprintf`, `faviconUrl`, … | Translator injection (`__: Translator` first arg), barrel `index.ts` | +| `packages-hooks` (`@probo/hooks`) | `packages/hooks` | 9 hooks: `usePageTitle`, `useFavicon`, `useToggle`, `useList`, … | Single-purpose, unit-testable, no peer-dep state libs | +| `packages-i18n` (`@probo/i18n`) | `packages/i18n` | Custom zero-dep i18n | Loaders return `{}` — i18n is **dormant**, language hard-coded `"en"` | +| `packages-coredata` (`@probo/coredata`) | `packages/coredata` | One shared enum: `TrustCenterDocumentAccessStatus` | Single shared status enum mirrored from Go side | +| `packages-emails` (`@probo/emails`) | `packages/emails` | 14 React Email templates → `dist/*.html.tmpl` + `dist/*.txt.tmpl` | Build via `tsx scripts/build.ts`; `//go:embed dist` in `pkg/mailer`; placeholders are Go template strings (`{{.GoTemplate}}`) inside JSX — no TS type-check | +| `packages-prosemirror` (`@probo/prosemirror`) | `packages/prosemirror` | Markdown ↔ ProseMirror node trees | `@tiptap/pm` schema, used by Tiptap editor in console | +| `packages-cookie-banner` (`@probo/cookie-banner`) | `packages/cookie-banner` | Vanilla web component, shadow DOM | Dual ESM + IIFE bundles; sessionStorage key uses `importFunction.toString()` (minification hazard) | +| `packages-vendors` (`@probo/vendors`) | `packages/vendors` | Static `data.json` (~100+ vendors) | MiniSearch consumer in `apps/console`; `data.d.ts` references undefined `CountryCode` type | +| `packages-react-lazy` (`@probo/react-lazy`) | `packages/react-lazy` | `lazy()` with retry + sessionStorage page-reload counter | Counter key uses `toString()` of import fn — cross-contamination hazard | +| `packages-n8n-node` (`@probo/n8n-node`) | `packages/n8n-node` | Community n8n node (236 files), resource × operation feature-slices | Export name MUST equal operation value string; IAM ops use `proboConnectApiRequest` | + +--- ## Canonical Examples | File | What it demonstrates | -|------|---------------------| -| `apps/console/src/routes/documentsRoutes.ts` | New-style route definitions using Loader components (no `withQueryRef`), nested tab routes, lazy loading | -| `apps/console/src/pages/organizations/documents/DocumentsPageLoader.tsx` | Canonical Loader component: `useQueryLoader` + `useEffect` + Suspense + CoreRelayProvider | -| `apps/trust/src/pages/DocumentPage.tsx` | Full page with colocated queries/mutations, typed error handling, `__typename` dispatch, `getPathPrefix()` usage | -| `packages/ui/src/Atoms/Badge/Badge.tsx` | Canonical UI atom: `tv()` variant factory, `asChild`/`Slot`, typed props, named export | -| `packages/helpers/src/audits.ts` | Standard domain helper: `as const` enum array, `getXLabel` with Translator, `getXVariant` for badge variants | +| --- | --- | +| `apps/console/src/pages/organizations/findings/FindingsPage.tsx` | The full current-pattern page: preloaded query, `usePaginationFragment` with `@connection(filters: [])`, `useFragment` row components, `useMutation` with `@deleteEdge`, `useTransition` for filter updates, `useToast` + `useConfirm`, `useTranslate` for every string | +| `apps/console/src/pages/organizations/findings/FindingsPageLoader.tsx` | `*PageLoader` shape: `CoreRelayProvider` → `useQueryLoader` in `useEffect` → `*PageSkeleton` while `queryRef` is null → `Suspense` wrapping `*Page` | +| `apps/console/src/pages/iam/organizations/people/routes.ts` | Colocated `routes.ts` (target arborescence) — spread into the parent route tree | +| `apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx` | Mutation-only page wrapped in `IAMRelayProvider` (no query, but provider still required) | +| `apps/console/src/environments.ts` | The two Relay environments — store, GC buffer, 1-minute query cache, `makeFetchQuery` from `@probo/relay` | +| `packages/ui/src/atoms/Button/` | Modern compound shape: flat exports, `tailwind-variants` in `variants.ts`, skeleton co-located and **does not import Root** | + +--- ## Topic Index -- [Patterns](./patterns.md) -- Relay data fetching, routing, component architecture, forms, error handling -- [Conventions](./conventions.md) -- TypeScript style, imports, naming, project structure -- [Testing](./testing.md) -- Storybook, vitest, e2e testing approach -- [Pitfalls](./pitfalls.md) -- common mistakes, deprecated patterns, things that break silently -- Module-specific notes: - - [apps/console](./module-notes/apps-console.md) -- dual Relay environments, snapshot mode, permission checks - - [apps/trust](./module-notes/apps-trust.md) -- path-prefix routing, auth flows, NDA signing - - [packages/emails](./module-notes/packages-emails.md) -- React Email + Go template hybrid - - [packages/n8n-node](./module-notes/packages-n8n-node.md) -- n8n community node conventions +- **[Patterns](./patterns.md)** — app layout, route definitions, Relay data flow, component shape, + `@probo/ui`, forms, error boundary chain, email templates, n8n node feature slice +- **[Conventions](./conventions.md)** — naming, icons, types, translator injection, barrel exports, + branding tokens, review-enforced standards +- **[Testing](./testing.md)** — Vitest, Testing Library, Storybook (`@probo/ui`), known coverage gaps +- **[Pitfalls](./pitfalls.md)** — Relay provider omissions, deprecated APIs, `@connection` filters, + prototype-chain quirks, packaging gotchas +- **[Module notes](./module-notes/)** — per-module specifics + +For everything cross-cutting (build, git, license headers, CI gates, GIDs, four-surface API rule, +PII-free logging) see [shared.md](../shared.md). ## Team Notes -_Reserved for manual additions by the team -- this section is preserved on refresh._ +_Reserved for manual additions by the team — this section is preserved on refresh._ ## Open Questions -1. The `hooks/graph/` directory in `apps/console` contains substantial legacy code with eslint-disable comments. Is there a migration plan to inline these into consuming components? -2. `TranslatorProvider` in both apps is a stub (TODO in source). Is i18n support planned, and what loader mechanism will be used? -3. Some layout loaders use `useQueryLoader` + `useEffect` instead of the router-loader preload pattern. Is this intentional for nested layouts that cannot use React Router loaders directly? -4. `@tanstack/react-query` is mounted at the root of both apps but used minimally. Can it be removed from `apps/trust`? -5. Should unrecognised GraphQL error codes in `@probo/relay` throw a generic error rather than being silently passed through? +- Migration timeline for `apps/console/src/routes/vendorRoutes.ts` off the deprecated + `loaderFromQueryLoader` / `withQueryRef`? +- Are we standardizing all forms on `react-hook-form` + Zod resolver, or keep the current mix + with plain `useState`? +- Plan to wire real translations through `@probo/i18n`, or remove the dormant infrastructure? +- Storybook coverage target for `@probo/ui` — current set is partial. diff --git a/.claude/guidelines/typescript-frontend/module-notes/apps-console.md b/.claude/guidelines/typescript-frontend/module-notes/apps-console.md index a3b18c718c..6df39e63da 100644 --- a/.claude/guidelines/typescript-frontend/module-notes/apps-console.md +++ b/.claude/guidelines/typescript-frontend/module-notes/apps-console.md @@ -1,78 +1,39 @@ -# Probo -- TypeScript Frontend -- apps/console - -> Module-specific notes for `apps/console` (`@probo/console`) -> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md) - -## Purpose - -The primary admin dashboard for compliance managers. A React 19 + Vite SPA (port 5173) that communicates with two GraphQL endpoints over Relay. It covers all compliance domains: risks, measures, documents, vendors, frameworks, audits, assets, data, tasks, processing activities, rights requests, snapshots, obligations, findings, and a public compliance/trust-center page manager. - -## Dual Relay Environments - -This is the only app with two Relay environments. See [patterns.md -- Relay Environments](../patterns.md#relay-environments-module-specific-appsconsole) for configuration details. - -**Critical rule**: IAM pages (`src/pages/iam/`) must use `IAMRelayProvider`. Organization pages (`src/pages/organizations/`) must use `CoreRelayProvider`. The Relay compiler config enforces this mapping at build time, but a runtime mismatch will cause silent schema errors. - -The `relay.config.json` defines two projects: - -```json -{ - "sources": { - "apps/console/src/pages/iam": "iam", - "apps/console/src": "core" - } -} -``` - -## Snapshot Mode - -Most list and detail pages support read-only snapshot viewing. This requires: - -1. **Duplicate routes** in the route file -- one for normal paths, one for `/snapshots/:snapshotId/...` paths -2. **Conditional rendering** -- check `Boolean(snapshotId)` from `useParams()` and hide all create/update/delete controls -3. **Adjusted URLs** -- breadcrumbs and internal links must use the snapshot path prefix - -Example from `apps/console/src/routes/assetRoutes.ts`: -```tsx -// Normal route -{ path: "assets", loader: loaderFromQueryLoader(({ organizationId }) => - loadQuery(coreEnvironment, assetsQuery, { organizationId, snapshotId: null })), -}, -// Snapshot route -{ path: "snapshots/:snapshotId/assets", loader: loaderFromQueryLoader(({ organizationId, snapshotId }) => - loadQuery(coreEnvironment, assetsQuery, { organizationId, snapshotId })), -}, -``` - -## Permission Fragments - -Inline permission queries in Relay fragments gate UI controls: - -```graphql -canCreate: permission(action: "core:risk:create") -canUpdate: permission(action: "core:risk:update") -canDelete: permission(action: "core:risk:delete") -``` - -These boolean fields control visibility of edit buttons, delete actions, and create dialogs. There are no separate permission hooks. - -## ViewerMembershipLayout - -`apps/console/src/pages/iam/organizations/ViewerMembershipLayout.tsx` is the top-level authenticated shell. It: -1. Wraps `IAMRelayProvider` for its own membership query -2. Mounts `CoreRelayProvider` for child organization pages -3. Provides `CurrentUser` context (email, fullName, role) to the entire app - -## Key Abstractions - -| Abstraction | File | Purpose | -|-------------|------|---------| -| `loaderFromQueryLoader` | `@probo/routes` | Bridges React Router loaders with Relay preloaded queries | -| `useMutationWithToasts` | `src/hooks/useMutationWithToasts.ts` | **DEPRECATED** -- use `useMutation` + `useToast` | -| `SortableTable` | `src/components/SortableTable.tsx` | Paginated, sortable list with refetch support | -| `OrganizationErrorBoundary` | `src/components/OrganizationErrorBoundary.tsx` | Catches auth/assumption errors and redirects | -| `useOrganizationId` | `src/hooks/useOrganizationId.ts` | Extracts `:organizationId` from route params | - -## Legacy: hooks/graph/ Directory - -The `src/hooks/graph/` directory contains shared Relay queries, fragments, and mutations (e.g., `RiskGraph.ts`, `AssetGraph.ts`). This is **legacy code** with `eslint-disable` comments for `relay/must-colocate-fragment-spreads`. New code must colocate all GraphQL operations in the consuming component file. +# Probo — TypeScript Frontend — apps/console + +The main compliance SPA. **437 TS/TSX files**, React 19 + Relay 19 + Vite + react-router v7. Two +Relay environments coexist (`coreEnvironment` for `/api/console/v1/graphql`, `iamEnvironment` for +`/api/connect/v1/graphql`); they are split at the **Vite/Babel level** so fragments under +`src/pages/iam/**` compile against `src/__generated__/iam/` and everything else against +`src/__generated__/core/`. The route tree is built in `src/routes.tsx` and assembled from a mix of +**legacy** `src/routes/*Routes.ts` files (deprecated `loaderFromQueryLoader` / `withQueryRef` +pattern) and the **target** colocated `routes.ts` files inside each page folder. + +## Key files + +- `apps/console/src/main.tsx` — entry, mounts `QueryClientProvider`, `TranslatorProvider`, + `RouterProvider`. +- `apps/console/src/environments.ts` — both Relay environments + `Network.create` wrapping + `makeFetchQuery` from `@probo/relay`. +- `apps/console/src/providers/CoreRelayProvider.tsx`, + `apps/console/src/providers/IAMRelayProvider.tsx` — the only correct ways to mount Relay. +- `apps/console/src/routes.tsx` — top-level route tree, spreads colocated `routes.ts` arrays. +- `apps/console/src/pages/organizations/findings/` — **canonical page folder** to copy from. +- `apps/console/src/components/RootErrorBoundary.tsx` — typed-error redirect chain. +- `apps/console/src/hooks/useOrganizationId.ts` — read `:organizationId` from router params. +- `apps/console/src/types.ts` — `NodeOf`, `ItemOf`. + +## How to add a new page + +1. Create folder `src/pages///`. +2. Add `routes.ts` exporting an `AppRoute[]` (lazy import the loader). +3. Spread the array into the parent route in `src/routes.tsx`. +4. Implement `PageLoader.tsx` (Relay provider + `useQueryLoader` + Suspense wrap). +5. Implement `Page.tsx` (`usePreloadedQuery`, fragments). +6. Implement `PageSkeleton.tsx` (synchronous, no Relay calls — see + [pitfalls.md § 10](../pitfalls.md#10-skeleton-importing-root)). +7. Sub-components in `_components/`. + +## Top pitfalls + +1. Wrong Relay provider for the path (Core vs IAM) — see [pitfalls.md § 1](../pitfalls.md#1-missing-relay-provider-in-a-pageloader). +2. Omitting `filters: []` on `@connection` — see [pitfalls.md § 3](../pitfalls.md#3-omitting-filters--on-connection). diff --git a/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md b/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md index 24086d926c..c0739933ae 100644 --- a/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md +++ b/.claude/guidelines/typescript-frontend/module-notes/apps-trust.md @@ -1,73 +1,32 @@ -# Probo -- TypeScript Frontend -- apps/trust +# Probo — TypeScript Frontend — apps/trust -> Module-specific notes for `apps/trust` (`@probo/trust`) -> For stack-wide patterns, see [patterns.md](../patterns.md) and [conventions.md](../conventions.md) +Public **unauthenticated** trust-center SPA (~50 TS/TSX files). Visitors view a tenant's published +trust report, request access to gated documents, sign NDAs, and authenticate via magic-link or +OIDC. Backed by `/api/trust/v1/graphql`. Most queries take a public token / slug as a variable +rather than relying on a session cookie. -## Purpose +The auth lazy-pages (magic-link consume, OIDC callback, NDA signature) each mount **their own +Relay environment**. This isolation is intentional (different auth states, different cache +lifetimes) but means a mutation in an auth lazy-page does **not** update the main layout's Relay +store. -A public-facing trust center React SPA (port 5174) that exposes an organization's compliance posture to external visitors. It is read-only except for the auth and NDA signing flows. Pages include Overview, Documents, Subprocessors, Updates, and NDA signing. +## Key files -## Single Relay Environment +- `apps/trust/src/main.tsx` — entry. +- `apps/trust/src/environments.ts` — primary trust environment. +- `apps/trust/src/pages/auth/*` — magic-link / OIDC / NDA isolated bundles. +- `apps/trust/src/pages//` — trust report pages, public-token-keyed. -Unlike the console app, trust uses a single Relay environment (`consoleEnvironment`) pointing at `/api/trust/v1/graphql`. Defined in `apps/trust/src/providers/RelayProviders.tsx`. +## How to extend -## Path Prefix Routing +- Adding a new public-facing page: same `*PageLoader` + `*Page` + `*PageSkeleton` pattern as + `apps/console`. Use the trust environment provider. +- Adding an auth flow: branch a new lazy-page under `pages/auth/` with its own environment if + state isolation is required. Otherwise share the trust environment. -The trust app supports two base URL modes: +## Top pitfalls -| Mode | Base Path | How detected | -|------|-----------|-------------| -| Probo-hosted | `/trust/{slug}` | URL matches `/trust/[^/]+` | -| Custom domain | `/` | Default fallback | - -Detection happens in `apps/trust/src/routes.tsx` via `getBasename()`. The `createBrowserRouter` receives the detected basename. - -**Critical rule**: Every manually constructed URL must use `getPathPrefix()` from `apps/trust/src/utils/pathPrefix.ts`. Never hardcode `/trust/` or `/`. This applies to redirect URLs, continue URLs, and all navigation targets. See [pitfalls.md -- Trust Center Path Prefix](../pitfalls.md#13-hardcoding-paths-without-getpathprefix-appstrust). - -## Auth Flow - -External visitors who need to access gated documents go through a multi-step auth flow: - -1. `RootErrorBoundary` catches `UnAuthenticatedError` and redirects to `/connect` with a `?continue=` URL -2. `/connect` (ConnectPage) -- magic link or OIDC login -3. `/verify-magic-link` -- validates the magic link token -4. `/full-name` -- captured if `FullNameRequiredError` is thrown -5. `/nda` -- NDA signing if `NDASignatureRequiredError` is thrown - -After successful auth, `useRequestAccessCallback` in MainLayout reads `request-document-id` / `request-report-id` / `request-file-id` search params and fires the corresponding access-request mutation automatically (post-login replay). - -## NDA Signing - -`apps/trust/src/pages/NDAPage.tsx` renders the NDA PDF, records signing events, and polls Relay every 1500ms until the signature status reaches `COMPLETED` before redirecting. There is currently no timeout/max-retry strategy for this polling. - -## TrustCenterProvider - -`apps/trust/src/providers/TrustCenterProvider.tsx` stores the `currentTrustCenter` Relay data from `MainLayout` so child components can access it without prop-drilling. Sub-pages like `OverviewPage` read trust center data from `useOutletContext` (inherited from MainLayout), not from their own query. - -## Route Organization - -All routes are defined in a single `apps/trust/src/routes.tsx` file (no separate route slice files like console). Routes are grouped: - -- **Auth routes**: Wrapped in `AuthLayout` (connect, verify-magic-link, full-name) -- **Content routes**: Wrapped in `MainLayout` with `RootErrorBoundary` (overview, documents, subprocessors, updates) -- **Standalone routes**: NDA page, document detail page - -## Key Abstractions - -| Abstraction | File | Purpose | -|-------------|------|---------| -| `getPathPrefix` | `src/utils/pathPrefix.ts` | Computes URL prefix for Probo-hosted vs custom domain mode | -| `RootErrorBoundary` | `src/components/RootErrorBoundary.tsx` | Typed error catch + auth redirect with continue URL | -| `useRequestAccessCallback` | `src/hooks/useRequestAccessCallback.ts` | Post-login access request replay from URL search params | -| `TrustCenterProvider` | `src/providers/TrustCenterProvider.tsx` | React context for trust center data from layout | - -## Differences from Console - -| Aspect | Console | Trust | -|--------|---------|-------| -| Relay environments | 2 (core + IAM) | 1 (trust) | -| Mutations | Frequent (CRUD for all entities) | Rare (access requests, NDA signing) | -| Route files | Separate per domain | Single `routes.tsx` | -| Snapshot mode | Yes (read-only views) | No (always read-only) | -| Permission fragments | Yes (`canCreate`, `canUpdate`, `canDelete`) | No (public read-only) | -| Auth errors | `UnAuthenticatedError`, `AssumptionRequiredError` | `UnAuthenticatedError`, `FullNameRequiredError`, `NDASignatureRequiredError` | +1. **Open-redirect on `continue` URL** — must validate same-origin + path-prefix before + `window.location.href = continue`. See [pitfalls.md § 6](../pitfalls.md#6-appstrust-open-redirect-via-unvalidated-continue-url). +2. **Isolated Relay stores in auth lazy-pages** — mutations don't update the main layout. See + [pitfalls.md § 8](../pitfalls.md#8-appstrust-isolated-relay-stores-in-auth-lazy-pages). diff --git a/.claude/guidelines/typescript-frontend/module-notes/packages-cookie-banner.md b/.claude/guidelines/typescript-frontend/module-notes/packages-cookie-banner.md new file mode 100644 index 0000000000..47c2a0aace --- /dev/null +++ b/.claude/guidelines/typescript-frontend/module-notes/packages-cookie-banner.md @@ -0,0 +1,33 @@ +# Probo — TypeScript Frontend — @probo/cookie-banner + +Vanilla **web component** (no React) that shows a cookie-consent banner on the marketing site and +inside the app shell. Implemented as a custom element with **shadow DOM** for style isolation. +Ships **two bundles**: ESM (for module-aware consumers) and IIFE (for `