Architecture epic, foundation step 5 of 5. Depends on A3 and A4. This is the template every later domain copies (commandment 35: "start new work from a reference domain"). Labels is small, used everywhere, and has both reads and writes, so it covers every layer.
io.paradaux.tasklie.labelslabels/model/Label.java: a record with its behaviour (colour validation, name normalisation).labels/persistence/LabelRepository.java: JdbcClient, named parameters, every query scoped by tenant_id, rows mapped to Label. Optional for lookups.labels/usecase/: ListLabels, CreateLabel, RenameLabel, RecolourLabel, MergeLabels and DeleteLabel. Each is one class with one public method, execute(ctx, input), returning Result<…, LabelError>.labels/LabelError.java: a sealed set of domain errors with HTTP statuses (NotFound, NameTaken, Forbidden, Invalid).labels/LabelsFacade.java: the only public entry and the transaction boundary. It checks permissions and composes use cases.labels/api/LabelController.java (REST under /api/public/w/{ws}/labels, request records with @Valid)labels/mcp/LabelMcpTools.java, which moves the label tools out of TaskMcpTools and calls the same facadePostgresTaskSystem. Other domains that need labels call LabelsFacade.LabelRepository with Testcontainers Postgres, including a cross-tenant isolation testapps/web/domains/labelsdata/server.ts (listLabels(ws)) and data/hooks.ts (useLabels, useCreateLabel…), built on the generated client.lib/sql/*) with these. Prune the related ESLint suppressions.Add a section to docs/architecture.md, "Reference domain: labels", that maps each commandment to the file implementing it. Later agents copy from it.
labels has zero violations, and the frozen store shrinks (the PostgresTaskSystem label SQL is gone).Plan (agent, branch par-413-labels-reference):
PermissionKeys, WorkspaceAccess port (slug -> workspace-scoped RequestContext, implemented in security/ so labels never imports it), McpToolProvider/McpToolSupport (context-based MCP tool base, builds RequestContext from the transport context).labels domain: model/Label, persistence/LabelRepository (JdbcClient, tenant-scoped), six use cases, sealed LabelError, LabelsFacade (the only transactional entry), api/LabelController (/api/public/w/{ws}/labels), mcp/LabelMcpTools (same tool names). Label SQL removed from PostgresTaskSystem/WorkspaceAdminService; remaining callers use the facade. One additive migration (label.description) because the PAR-400 design shows a description.app/api/private/backend/[...path] (only /api/public/w/**), createBrowserClient pointed at it, domains/labels/{data,components}, Settings -> Labels page, Kysely label reads replaced, ESLint suppressions pruned.docs/architecture.md (commandment -> file). Ratchet deltas (ArchUnit store + ESLint suppressions) reported in the PR.PR open: https://github.com/ParadauxIO/taskl.ie/pull/45 (ArchUnit store -58 lines, ESLint suppressions unchanged at 36 files / 89, see the PR for why).
Merged into rebuild (PR #45, after one review round). This is now the template for every domain migration. See "Reference domain: labels" in docs/architecture.md.
Ratchet: the ArchUnit store went from 1986 to 1928 lines (−58). The 10 added lines are existing violations whose text changed when constructors gained a LabelsFacade parameter; the orchestrator checked them. ESLint suppressions are unchanged, because the issue and board queries still join labels until PAR-414.
Decisions every later domain copies
RequestContext through the platform.WorkspaceAccess port (implemented in security/WorkspaceAccessService), so domains never import security.issue.read. Writes always require membership. Private workspaces stay closed. (Fixed in review; this is the PAR-423 rule in its minimal form.)@InWorkspace RequestContext ctx, which resolves {slug} and permissions.Rollback.onErr when they return an Err after writing.McpToolProvider bean extending McpToolSupport.app/api/private/backend/[...path]:
/api/public/w/<slug>/…KEYCLOAK_ISSUER (set in the gitops base) for verified actors.Deferred: a unique index on lowercased label names (uniqueness is enforced in the use case for now), and the issue↔label association SQL (moves in PAR-414).
Added scope (from the PAR-411 review): browser-side API calls go through a token-attaching proxy.
The browser must never hold a Keycloak token or service key. Client hooks (
data/hooks.ts) need authenticated calls, so:apps/web/app/api/private/backend/[...path]/route.ts. The UI owns/api/private/**. It forwards the method, query and body toTASKLIE_API_BASE+/api/public/..., and attaches the session's actor token server-side with the same plumbing aslib/api/client.server.ts(getActorToken,actorHeaders). With no session, it forwards as guest./api/public/w/**paths (no webhooks, no MCP). Strip hop-by-hop headers, and pass the status and body through unchanged.createBrowserClient()inlib/api/client.tsat/api/private/backendby default.Labels' client mutations (create, rename, recolour, merge, delete) are the first users, and every later domain's hooks reuse this proxy. Document it in
domains/README.md.