181fb1d4dd
First end-to-end Renovate run (after switching to direct docker run)
extracted dependencies but failed to commit any update branches with
`fatal: empty ident name not allowed`, then aborted the repo with
`Lock file error - aborting`. Two root causes:
- The bot user had an email but no Full Name on its Gitea profile,
so Renovate could not derive a complete git author identity. After
enough commit failures Renovate gives up on the repository.
- Renovate also warned `GitHub token is required for some
dependencies` and could not resolve `containerbase/node-prebuild`
releases (the Node binary it pulls dynamically for lockfile
maintenance) — anonymous github.com rate limit (60 req/h) was the
bottleneck.
Fixes:
1. Pin `gitAuthor` explicitly in `renovate.json` so the identity does
not depend on out-of-band Gitea profile state. The Full Name was
also set on the bot profile for consistency with Gitea's UI.
2. Pass `RENOVATE_GITHUB_COM_TOKEN` from a repo secret. The secret is
named `GITHUBCOM_TOKEN` (no underscore between GITHUB and COM)
because Gitea reserves the `GITHUB_*` secret namespace for the
built-in `${{ github.* }}` context. The token is a zero-scope
PAT — anonymous-equivalent rights, only useful for the higher
authenticated rate limit (5 000 req/h).
`docs/development.md` is updated with the new bot-onboarding steps
(Full Name, GITHUBCOM_TOKEN setup).
367 lines
26 KiB
Markdown
367 lines
26 KiB
Markdown
# Development guide
|
||
|
||
This document is the day-to-day reference for working on `apf_portal`. It covers the repo layout, the prerequisites, the initial setup from a fresh clone, and the commands you'll run during a typical development cycle. It is meant to grow — add sections as the team's workflow does.
|
||
|
||
For decision rationale, see the [ADRs](decisions/). For onboarding the local environment (terminal, Node, pnpm), see [setup/](setup/).
|
||
|
||
---
|
||
|
||
## 1. Repo layout
|
||
|
||
```
|
||
apf_portal/
|
||
├── .gitea/workflows/ # CI pipelines (ADR-0015)
|
||
│ ├── ci.yml # per-PR + push to main: check / scan / commits / perf / a11y
|
||
│ └── security-scheduled.yml # weekly full-tree scan + prod Lighthouse
|
||
├── .github/ # Nx AI-tooling skills, prompts, agents (Nx-managed)
|
||
├── .husky/ # local git hooks (ADR-0007)
|
||
│ ├── pre-commit # → pnpm exec lint-staged
|
||
│ └── commit-msg # → pnpm exec commitlint
|
||
├── apps/
|
||
│ ├── portal-shell/ # Angular 21 SPA (zoneless, standalone, Signals, Vitest, SCSS)
|
||
│ │ ├── public/ # static assets
|
||
│ │ ├── src/ # entry, app config, routes, styles
|
||
│ │ ├── postcss.config.js # Tailwind PostCSS plugin
|
||
│ │ └── project.json # Nx project config (build, serve, test, lint targets)
|
||
│ ├── portal-shell-e2e/ # Playwright e2e for portal-shell
|
||
│ ├── portal-bff/ # NestJS 11 BFF (Express adapter, ValidationPipe, Jest)
|
||
│ │ ├── src/ # main, app module, controllers, services
|
||
│ │ ├── prisma/schema.prisma # Prisma 7 schema (postgresql)
|
||
│ │ ├── prisma.config.ts # Prisma 7 TS config (loads DATABASE_URL from .env)
|
||
│ │ ├── .env.example # env-vars catalog (committed); .env stays gitignored
|
||
│ │ └── project.json
|
||
│ └── portal-bff-e2e/ # Jest e2e for portal-bff
|
||
├── libs/
|
||
│ ├── feature/<name>/ # vertical feature libs (e.g. feature-auth)
|
||
│ └── shared/<scope>/ # cross-cutting libs (tokens, ui, util)
|
||
├── docs/
|
||
│ ├── README.md # doc index
|
||
│ ├── decisions/ # ADRs (MADR 4.0.0)
|
||
│ ├── setup/ # local-environment onboarding (Zsh, pnpm, Nx workspace)
|
||
│ └── development.md # this file
|
||
├── notes/ # personal scratchpad (gitignored)
|
||
├── CLAUDE.md # project rules + architecture summary
|
||
├── commitlint.config.cjs # Conventional Commits config
|
||
├── eslint.config.mjs # workspace ESLint with module boundaries
|
||
├── lighthouserc.js # Lighthouse CI thresholds (ADR-0017)
|
||
├── nx.json # Nx workspace config
|
||
├── package.json # workspace deps + scripts
|
||
├── pnpm-workspace.yaml # apps/* + libs/**
|
||
├── tsconfig.base.json # shared TS strict config
|
||
└── vitest.workspace.ts # Vitest workspace projects
|
||
```
|
||
|
||
The conventions that govern this layout are recorded in:
|
||
|
||
- [ADR-0002](decisions/0002-adopt-nx-monorepo-apps-preset.md) — Nx workspace shape
|
||
- [ADR-0003](decisions/0003-workspace-and-app-naming-convention.md) — naming convention (`portal-shell`, `portal-bff`, `feature-<name>`, `shared-<scope>`)
|
||
- [ADR-0007](decisions/0007-pre-commit-hooks-and-conventional-commits.md) — local hooks
|
||
- [ADR-0015](decisions/0015-cicd-gitea-actions.md) — CI/CD shape
|
||
|
||
---
|
||
|
||
## 2. Prerequisites
|
||
|
||
A working dev machine for `apf_portal` needs:
|
||
|
||
| Tool | Why | How |
|
||
| -------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||
| **WSL 2 + Debian** (Windows) or Linux/macOS native | All commands assume a POSIX shell | see [setup/01](setup/01-wsl-terminal-setup.md) |
|
||
| **Node.js 24** (latest LTS) | Runtime, pinned in `.nvmrc` | `nvm install 24 && nvm use` (see [setup/02](setup/02-dev-web-stack.md)) |
|
||
| **pnpm 10+** | Mandatory package manager (no npm/yarn lockfile) | `corepack enable && corepack prepare pnpm@latest --activate` |
|
||
| **Git ≥ 2.40** | Husky 9 + signed commits eventually | usually default |
|
||
| **mkcert** | Local HTTPS for cookie-prefix `__Host-` (ADR-0009) | `apt install mkcert` then `mkcert -install` |
|
||
| **Trivy** _(optional, locally)_ | Dep vuln scan when running `ci:scan` locally; CI uses an action | `apt install trivy` or [trivy install docs](https://trivy.dev/) |
|
||
| **gitleaks** _(optional, locally)_ | Same pattern; CI uses an action | `apt install gitleaks` or [gitleaks install docs](https://github.com/gitleaks/gitleaks#installing) |
|
||
| **Docker** | For Postgres + Redis containers in dev (until on-prem infra ADR lands) | Docker Desktop on Windows, Docker Engine on Linux |
|
||
|
||
Work inside the WSL filesystem (`~/Works/...`), never `/mnt/c/...` — the latter has severe I/O penalties that break Nx caching.
|
||
|
||
---
|
||
|
||
## 3. Initial setup from a fresh clone
|
||
|
||
```bash
|
||
git clone gitea@git.unespace.com:julien/apf_portal.git
|
||
cd apf_portal
|
||
|
||
# Install deps (also runs `husky` to wire git hooks)
|
||
pnpm install
|
||
|
||
# Generate the Prisma client (until you set up the DB it errors on
|
||
# missing DATABASE_URL — that's expected; the generation only reads
|
||
# the schema, not the DB).
|
||
cd apps/portal-bff && pnpm exec prisma generate && cd ../..
|
||
|
||
# Sanity check
|
||
pnpm nx run-many -t lint test build
|
||
```
|
||
|
||
For the BFF to actually run end-to-end, you'll also need:
|
||
|
||
```bash
|
||
# .env from .env.example, then fill in DATABASE_URL with your local Postgres
|
||
cp apps/portal-bff/.env.example apps/portal-bff/.env
|
||
|
||
# Local Postgres via Docker (one-liner; will be replaced by a Docker Compose file in a later doc)
|
||
docker run -d --name apf-postgres -p 5432:5432 \
|
||
-e POSTGRES_USER=portal -e POSTGRES_PASSWORD=portal -e POSTGRES_DB=portal_dev \
|
||
postgres:17-alpine
|
||
```
|
||
|
||
A proper local-dev infra spec (Postgres HA-like, Redis, OTel collector) will land with the on-prem infrastructure ADR; in the meantime the one-liner above is sufficient to run the BFF.
|
||
|
||
---
|
||
|
||
## 4. Daily commands
|
||
|
||
### Run the apps
|
||
|
||
```bash
|
||
pnpm nx serve portal-shell # http://localhost:4200 (Angular dev server)
|
||
pnpm nx serve portal-bff # http://localhost:3000/api (NestJS)
|
||
```
|
||
|
||
Both can run in parallel in two terminals; the SPA proxies API calls to the BFF in dev.
|
||
|
||
### Test
|
||
|
||
```bash
|
||
pnpm nx test portal-shell # Vitest (single run; --configuration=watch for watch mode)
|
||
pnpm nx test portal-bff # Jest
|
||
|
||
pnpm nx run-many -t test # all projects
|
||
pnpm nx affected -t test # only projects affected since main
|
||
```
|
||
|
||
Run a single Vitest file:
|
||
|
||
```bash
|
||
pnpm nx test portal-shell --testFile=src/app/app.spec.ts
|
||
```
|
||
|
||
### Lint, type-check, format
|
||
|
||
```bash
|
||
pnpm nx lint portal-shell # one project
|
||
pnpm nx run-many -t lint # all projects
|
||
pnpm nx affected -t lint # affected only
|
||
pnpm nx affected -t lint --fix # auto-fix where possible
|
||
|
||
pnpm nx affected -t type-check # explicit type-check (independent of test/build)
|
||
|
||
pnpm nx format:write # apply Prettier
|
||
pnpm nx format:check # CI-style verification
|
||
```
|
||
|
||
### Build
|
||
|
||
```bash
|
||
pnpm nx build portal-shell # development build
|
||
pnpm nx build portal-shell --configuration=production # production build
|
||
pnpm nx run-many -t build
|
||
pnpm nx affected -t build
|
||
```
|
||
|
||
### Generate
|
||
|
||
```bash
|
||
# Component in portal-shell
|
||
pnpm nx g @nx/angular:component <name> --project=portal-shell --standalone
|
||
|
||
# Service / controller / module in portal-bff
|
||
pnpm nx g @nx/nest:service <name> --project=portal-bff
|
||
pnpm nx g @nx/nest:controller <name> --project=portal-bff
|
||
|
||
# New shared lib (TS-only, consumable by both apps)
|
||
pnpm nx g @nx/js:library --name=shared-<scope> --directory=libs/shared/<scope> \
|
||
--bundler=tsc --unitTestRunner=vitest \
|
||
--tags="scope:shared,type:shared" --no-interactive
|
||
|
||
# New Angular feature lib (front-only)
|
||
pnpm nx g @nx/angular:library --name=feature-<name> --directory=libs/feature/<name> \
|
||
--standalone=true --unitTestRunner=vitest-analog \
|
||
--tags="scope:portal-shell,type:feature" --no-interactive
|
||
```
|
||
|
||
> Sweep generated files for `process.env.X` (dot notation) → `process.env['X']` (bracket notation), required by the strict-TS option `noPropertyAccessFromIndexSignature: true`. The Nx generators don't emit bracket form.
|
||
|
||
### Prisma
|
||
|
||
```bash
|
||
# Regenerate the typed client after schema changes
|
||
cd apps/portal-bff && pnpm exec prisma generate && cd ../..
|
||
|
||
# Create and apply a migration in dev
|
||
cd apps/portal-bff && pnpm exec prisma migrate dev --name <migration-name> && cd ../..
|
||
|
||
# Deploy migrations in prod (run by deploy pipeline, not locally)
|
||
cd apps/portal-bff && pnpm exec prisma migrate deploy && cd ../..
|
||
|
||
# Inspect the dev DB
|
||
cd apps/portal-bff && pnpm exec prisma studio && cd ../..
|
||
```
|
||
|
||
### CI scripts (runnable locally)
|
||
|
||
Mirror what the CI does on every PR:
|
||
|
||
```bash
|
||
pnpm ci:check # nx affected -t format:check lint test build
|
||
pnpm ci:audit # pnpm audit --audit-level=moderate
|
||
pnpm ci:commits # commitlint on the PR commit range (uses $COMMIT_LINT_FROM, defaults to origin/main)
|
||
pnpm ci:perf # production build + Lighthouse CI against the static-served bundle
|
||
```
|
||
|
||
`ci:scan` (Trivy + gitleaks) is currently invoked from CI YAML rather than as a `pnpm` script — those tools are Go binaries without clean npm wrappers. Run them locally if you've installed the binaries.
|
||
|
||
---
|
||
|
||
## 5. Dependency updates (Renovate)
|
||
|
||
[Renovate](https://docs.renovatebot.com/) runs as a scheduled workflow ([`.gitea/workflows/renovate.yml`](../.gitea/workflows/renovate.yml)) and opens PRs against `main` for dependency updates. Daily at 03:00 UTC, plus on-demand via `workflow_dispatch`.
|
||
|
||
Behaviour is controlled by [`renovate.json`](../renovate.json) at the repo root: groupings (Angular, Nx, NestJS, Prisma, Vitest, TypeScript tooling, ESLint, SWC, Tailwind), Conventional-Commits-compatible commit messages (`chore(deps): …` / `fix(deps): …` for vulnerability fixes), weekly lockfile maintenance, OSV.dev as the vulnerability data source.
|
||
|
||
### One-time bot onboarding
|
||
|
||
Renovate authenticates as a dedicated bot user. Setup is manual on Gitea — done once per Gitea instance, then the workflow runs unattended.
|
||
|
||
1. **Create a bot user.** Site Administration → Users → Create User. Suggested name: `apf-portal-bot`. Strong password, mark as **non-admin** (least privilege).
|
||
2. **Set the bot's Full Name** in its Gitea profile (User Settings → Profile → Full Name, e.g. `APF Portal Bot`). Without it, Renovate's git commits fail with `empty ident name not allowed`. The `gitAuthor` in `renovate.json` is the explicit override, but keeping the profile value consistent avoids confusion when reading commit history in Gitea's UI.
|
||
3. **Add the bot as a collaborator** on this repo with **Write** access (Settings → Collaborators). Without write, Renovate can't push branches.
|
||
4. **Generate a PAT for the bot** (`RENOVATE_TOKEN`). Sign in as the bot, then User Settings → Applications → Generate New Token. Scopes needed: read/write `repository`, read/write `issue`, read `user`. Avoid `admin`.
|
||
5. **Store the PAT as a repo secret.** Settings → Actions → Secrets → New Secret. Name: `RENOVATE_TOKEN`. Value: the token from step 4.
|
||
6. **Generate a zero-scope GitHub.com PAT** (`GITHUBCOM_TOKEN`). On github.com (any account, e.g. yours): Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic). **Do not tick any scope** — anonymous-equivalent rights are enough; the token only buys Renovate the higher authenticated rate limit (5 000 req/h vs 60 req/h) for resolving GitHub-hosted Action versions and `containerbase/node-prebuild` binaries used during lockfile maintenance.
|
||
7. **Store it as a repo secret named `GITHUBCOM_TOKEN`** (Gitea reserves the `GITHUB_*` secret namespace for the built-in `${{ github.* }}` context, so an underscore between `GITHUB` and `COM` is rejected).
|
||
8. **Sign out and forget both tokens locally.** They are now only retrievable via the secret store.
|
||
|
||
To **rotate** either token: regenerate at the matching step, update the secret. The schedule keeps running unattended.
|
||
|
||
### Triggering manually
|
||
|
||
Repo → Actions → "Renovate" workflow → Run workflow. Useful when you've just changed `renovate.json` and want the next pass to happen immediately rather than wait for the next 03:00 UTC tick.
|
||
|
||
### Reviewing Renovate PRs
|
||
|
||
- Each Renovate PR is gated by the same CI as a human PR — `check`, `scan`, `commits`, `perf`, `a11y`. Don't merge until all are green.
|
||
- The "Renovate Dependency Dashboard" issue (auto-created on first run) lists every pending update grouped by status. Use it to triage which PRs to expedite.
|
||
- For a major bump that introduces breaking changes, **don't reflexively merge**: read the changelog, then either accept the work or close the PR with a "rejected" label. Renovate respects that label and won't keep re-opening the same major.
|
||
- **Adding or removing** a dependency belongs in a feature PR, not in Renovate's scope. Renovate only updates _versions_ of existing deps.
|
||
|
||
---
|
||
|
||
## 6. Conventional commit cycle
|
||
|
||
1. Branch from `main` with a short slug:
|
||
|
||
```bash
|
||
git switch -c feat/portal-shell/auth-login # or fix/..., chore/..., docs/...
|
||
```
|
||
|
||
2. Commit using **Conventional Commits**. The local `commit-msg` hook (commitlint) rejects anything else.
|
||
|
||
```
|
||
feat(portal-shell): add login flow stub
|
||
fix(portal-bff): correct env var bracket access
|
||
chore: bump @nx/* to 22.7.2
|
||
docs(decisions): add ADR-0018 for security baseline
|
||
```
|
||
|
||
`pre-commit` runs `lint-staged` → Prettier on staged files. Lint and tests stay in CI.
|
||
|
||
3. Push and open a PR against `main`. The CI runs:
|
||
- `check` (lint, type-check, test, build on affected)
|
||
- `scan` (audit, Trivy, gitleaks)
|
||
- `commits` (commitlint on the PR commit range)
|
||
- `perf` (Lighthouse on the production bundle)
|
||
- `a11y` (axe-core; placeholder until first real screens)
|
||
|
||
All five must be green to merge. PR title must itself be a Conventional Commits message — it becomes the squash-merge subject ([ADR-0015](decisions/0015-cicd-gitea-actions.md)).
|
||
|
||
4. Squash-merge into `main`. Branch is auto-deleted. Linear history maintained.
|
||
|
||
5. To cut a release: tag `vX.Y.Z` on `main`. The `release.yml` workflow will pick it up (currently a stub; populated alongside the on-prem deploy ADR).
|
||
|
||
### PR conventions
|
||
|
||
The squash-merge subject on `main` is the **PR title**, not the individual commits on the feature branch (those collapse into the squash). Two practical consequences:
|
||
|
||
1. **The PR title must itself be a valid Conventional Commits message.** Same format as a commit message — `<type>(<scope>): <description>`, imperative mood, lowercase, no trailing period, target ≤ 70 chars. The CI `commits` job (commitlint on the PR commit range) catches violations.
|
||
2. **Individual commits on the feature branch can be exploratory.** The local `commit-msg` hook still validates each commit's format, but the squash makes granular history irrelevant on `main`. Granular history stays available in the PR for review.
|
||
|
||
#### Type vocabulary
|
||
|
||
| Type | When |
|
||
| ---------- | ----------------------------------------------- |
|
||
| `feat` | new user-facing feature or capability |
|
||
| `fix` | bug fix |
|
||
| `docs` | documentation only (no code) |
|
||
| `style` | formatting / whitespace (no logic change) |
|
||
| `refactor` | code change that is neither a fix nor a feature |
|
||
| `perf` | performance improvement |
|
||
| `test` | tests added or updated |
|
||
| `build` | build system, dependencies |
|
||
| `ci` | CI configuration |
|
||
| `chore` | maintenance, scaffolding, project metadata |
|
||
| `revert` | revert a previous commit |
|
||
|
||
#### Scope vocabulary (optional)
|
||
|
||
| Scope | Examples |
|
||
| ------------- | ----------------------------------------------------------- |
|
||
| App | `portal-shell`, `portal-bff` |
|
||
| Lib | `shared-tokens`, `shared-ui`, `shared-util`, `feature-auth` |
|
||
| Cross-cutting | `decisions` (ADR work), `docs`, `ci`, `deps` |
|
||
|
||
Scope is optional. Omit when the change spans too many areas to scope cleanly (e.g., a workspace-level rename).
|
||
|
||
#### PR body template
|
||
|
||
When a PR is opened against `main`, Gitea pre-populates the body from `.gitea/pull_request_template.md`:
|
||
|
||
- **Summary** — 1–3 bullets describing what changed.
|
||
- **Motivation** — why, with ADR / issue / incident links.
|
||
- **Implementation notes** — trade-offs, alternatives considered, follow-ups deferred.
|
||
- **Verification** — CI gates checked, manual test description, ADR / diagram update flags.
|
||
- **Related** — ADR-XXXX, related PRs, follow-up issues.
|
||
|
||
The template guides without enforcing — sections can be left blank when irrelevant. The point is to make "what does the reviewer need to know" explicit, not to add ceremony.
|
||
|
||
---
|
||
|
||
## 7. Where to look
|
||
|
||
| Question | Doc |
|
||
| ------------------------------------------- | ---------------------------------------------------------------------------- |
|
||
| Project rules and the why behind them | [CLAUDE.md](../CLAUDE.md) |
|
||
| All ADRs (decisions index) | [docs/decisions/README.md](decisions/README.md) |
|
||
| Initial environment setup (Zsh, Node, pnpm) | [docs/setup/](setup/) |
|
||
| RSSI briefing for ASVS / HDS / etc. | `notes/asvs-level-decision-briefing-rssi.md` (gitignored, personal) |
|
||
| The dev-team rationale for the UI stack | `notes/argumentaire-stack-ui-spartan-cdk-tailwind.md` (gitignored, personal) |
|
||
|
||
---
|
||
|
||
## 8. Sections to come — roadmap by phase
|
||
|
||
This doc starts as a phase-1 + cross-cutting reference. As features for later phases land, the corresponding sections below are filled in directly. Each entry is mapped to the ADR / implementation work that unlocks it, so a contributor can see when each section becomes real and what triggers it.
|
||
|
||
When a section grows beyond a short subsection, it is extracted to its own file under `docs/development/`. Per the documentation convention (see [README.md](README.md)), we group into a folder once we have at least three related files; this doc is then re-organised into an index pointing at the extracted files. Until then, all sections live here.
|
||
|
||
| Future section | Phase | Triggered by |
|
||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **Local infra recipe** — Docker Compose for Postgres, Redis, OTel Collector, and a Postgres-friendly viewer. | 2 / 3b | First feature that needs Redis (sessions, ADR-0010). Earlier if the BFF gains a meaningful amount of business code. |
|
||
| **Auth dev-loop** — Microsoft 365 Developer tenant configuration, MSAL Node connection, OIDC code-flow walkthrough, switching between dev and prod-like tenants. | 2 | Auth flow code lands ([ADR-0009](decisions/0009-auth-flow-oidc-pkce-msal-node.md)) once the dev tenant is provisioned by IT. |
|
||
| **Session inspection** — reading the Redis session store in dev, decrypting the AES-GCM `tokens` blob with the dev key, force-logout patterns. | 2 | Sessions module lands ([ADR-0010](decisions/0010-session-management-redis.md)). |
|
||
| **MFA step-up debugging** — triggering claims-challenge flows, verifying `mfaVerifiedAt` freshness, testing the SPA HTTP interceptor that handles 401 + claims challenge. | 2 | First `@RequireMfa()` route lands ([ADR-0011](decisions/0011-mfa-enforcement-entra-conditional-access.md)). |
|
||
| **Observability dev-loop** — running an OTel collector container locally, viewing traces (Tempo / Jaeger UI), reading Pino logs with `pino-pretty`, correlating front spans to BFF spans via `traceparent`. | 2 | OTel SDK setup lands ([ADR-0012](decisions/0012-observability-pino-opentelemetry.md)); needs the local collector from "Local infra recipe". |
|
||
| **Audit-log inspection workflow** — querying `audit.events` as `audit_reader`, joining with app logs by `trace_id`, validating the append-only role grants in dev. | 2 | Audit module lands ([ADR-0013](decisions/0013-audit-trail-separated-postgres-append-only.md)). |
|
||
| **Downstream API integration recipe** — adding a new `DownstreamApiConfig`, choosing the auth strategy (OBO vs service+assertion), wiring resilience policies, testing with a mocked downstream. | 2 | First downstream client lands ([ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md)). |
|
||
| **Component patterns library** — the in-house, spartan-style components (Angular CDK + Tailwind) as they ship, with a11y notes per component (keyboard model, ARIA, screen-reader expectations). | 5b suite | First non-placeholder component in `libs/shared/ui/`. |
|
||
| **a11y testing workflow** — running axe-core via Playwright locally, screen-reader testing notes (NVDA / VoiceOver / TalkBack), the APF user-panel cadence and how to triage findings. | 3a | First Playwright e2e suite touching real screens ([ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md)). |
|
||
| **Performance debugging** — running Lighthouse CI locally with full config, reading the HTML reports, using `source-map-explorer` to investigate bundle bloat, interpreting BFF p95/p99 from OTel. | 3a | Lighthouse already wired in CI ([ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md)); section grows when first real route is added to the critical-routes list. |
|
||
| **Debugging tips** — Angular DevTools, NestJS inspector, Prisma query log, OTel trace navigation, common gotchas. | cross | Accumulates organically as the team encounters them. |
|
||
| **Release workflow** — tag-driven release, what `release.yml` does, version bumping, changelog generation from Conventional Commits. | 3b | On-prem infrastructure ADR + populated `release.yml`. |
|
||
| **GitLab migration runbook** — when the org migrates Gitea → GitLab, how the workflows are ported, which level-2 sections of [ADR-0015](decisions/0015-cicd-gitea-actions.md) get superseded. | future | GitLab migration ADR (6–18 months horizon). |
|
||
| **Architecture overview diagrams** — high-level component diagrams, data-flow diagrams, trust boundaries (for security review). | cross | First major architecture review or onboarding cohort ≥ 3 contributors. |
|