cvx-kit 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ # `cvx-kit/components/foundation` — the audited command protocol
2
+
3
+ The Foundation is a Convex component that **owns zero tables and zero
4
+ capability** — its schema is empty by design. What it provides is the
5
+ application kernel: the `Command` protocol (validate → observe → execute →
6
+ audit, in one transaction), the `Query` kernel, and payload-free
7
+ `Observability`. It is declared **once** per app and everything destructures
8
+ from it; there is no separate command import.
9
+
10
+ ## Mounting and declaring
11
+
12
+ ```ts
13
+ // convex/convex.config.ts
14
+ import { defineApp } from 'convex/server'
15
+ import foundation from 'cvx-kit/components/foundation'
16
+
17
+ const app = defineApp()
18
+ app.use(foundation)
19
+ export default app
20
+ ```
21
+
22
+ ```ts
23
+ // convex/foundation.ts — declared once, the single kernel source
24
+ import { Foundation } from 'cvx-kit/components/foundation'
25
+ import { components } from './_generated/api'
26
+ import { writeAuditEntry } from './audit'
27
+
28
+ export const { Command, Query, observability } = new Foundation(
29
+ components.foundation,
30
+ {
31
+ observability: {
32
+ enabled: () => process.env.COMMAND_OBSERVABILITY_ENABLED === 'true',
33
+ classifyError: (error) => classify(error), // → { outcome: 'denied' | 'failed', errorCode }
34
+ writeAudit: (ctx, entry) => writeAuditEntry(ctx, entry),
35
+ },
36
+ },
37
+ )
38
+ ```
39
+
40
+ Host code consumes the Foundation **only through this facade** — never deep
41
+ imports of the component's internal modules.
42
+
43
+ ## Defining commands — per domain
44
+
45
+ Each domain declares a frozen registry of operations and derives typed
46
+ executors from it. `classification` and `audit()` are **mandatory per
47
+ operation** — auditing is type-enforced, not opt-in.
48
+
49
+ ```ts
50
+ // convex/domain/documents/commands.ts
51
+ import { z } from 'zod'
52
+ import { Command } from '../../foundation'
53
+ import { documents } from './schema'
54
+
55
+ const operations = {
56
+ 'documents.rename': Command.operation({
57
+ command: documents.commandInput.extend({ id: zid('documents') }),
58
+ result: z.object({ ok: z.literal(true) }).strict(),
59
+ classification: 'business',
60
+ audit: ({ command }) => ({
61
+ operation: 'documents.rename',
62
+ actorId: command.actorId,
63
+ aggregate: { type: 'document', id: command.id },
64
+ metadata: { title: command.title },
65
+ }),
66
+ }),
67
+ } as const
68
+
69
+ const commands = new Command<MutationCtx, typeof operations>(operations)
70
+
71
+ export const executeRename = commands.exec({
72
+ operation: 'documents.rename',
73
+ handler: async (ctx, command) => {
74
+ await ctx.db.patch(command.id, { title: command.title })
75
+ return { ok: true }
76
+ },
77
+ })
78
+ ```
79
+
80
+ The public API layer then wraps the executor in an `authMutation` and passes
81
+ `ctx.actor` in. Public functions stay thin adapters; domain logic lives behind
82
+ `execute*` functions.
83
+
84
+ ## What one execution does, in order
85
+
86
+ 1. **Parse the command** through the operation's `command` schema (strict zod).
87
+ 2. **Observe**: start the observation clock.
88
+ 3. **Permission check** — if the operation declares `permission`, the
89
+ Foundation's injected `checkPermission(ctx, { permission, operation })`
90
+ runs; throw to deny. Declaring a permission with no injected checker
91
+ **fails closed** (`COMMAND_PERMISSION_NOT_CONFIGURED`).
92
+ 4. **Default guard** — the registry-wide guard passed as the Command's second
93
+ argument (`new Command(operations, { guard })`), if any.
94
+ 5. **Operation guard** — the operation's own `guard(ctx, command)`:
95
+ preconditions like state-machine legality, ownership beyond roles, or
96
+ invariants over the parsed command. Throw to deny — nothing has run yet,
97
+ so a denial is always clean.
98
+ 6. **Run the handler**; its return value is parsed through the `result` schema
99
+ — outputs are validated too.
100
+ 7. **Audit**: call the operation's `audit({ command, result }, ctx)`. If it
101
+ returns non-null, the entry (plus the operation's `classification`) is
102
+ written through the injected `writeAudit` — **in the same transaction** as
103
+ the handler's writes. Returning `null` skips the audit (the ontology
104
+ convention: failures are not audited). When the operation declares
105
+ `aggregates: [...]`, an audit whose `aggregate.type` is not in that
106
+ allowlist throws (`COMMAND_AGGREGATE_NOT_DECLARED`) — the audit
107
+ vocabulary is enforced, not advisory. The per-operation catalog is
108
+ introspectable as `commands.aggregates` for tests.
109
+ 8. **Emit the observation**: `{ operation, classification, outcome,
110
+ errorCode?, durationMs }` — completed, denied, or failed per
111
+ `classifyError`.
112
+
113
+ ### Guards and permissions
114
+
115
+ ```ts
116
+ // convex/foundation.ts — permission semantics are host policy, injected once
117
+ export const { Command, Query, observability } = new Foundation(
118
+ components.foundation,
119
+ {
120
+ observability: { ... },
121
+ checkPermission: (ctx, { permission }) =>
122
+ requirePermission(ctx.identity, permission), // throw to deny
123
+ },
124
+ )
125
+
126
+ // domain/<module>/commands.ts
127
+ const operations = {
128
+ '<entities>.publish': Command.operation({
129
+ command: <entities>.commandInput.extend({ id: zid('<entities>') }),
130
+ result: z.object({ ok: z.literal(true) }).strict(),
131
+ classification: 'business',
132
+ permission: '<domain>.manage', // checked first
133
+ aggregates: ['<aggregate-type>'], // audit vocabulary allowlist
134
+ guard: async (ctx, command) => { // precondition, pre-handler
135
+ const row = await requireTenantReference(ctx.tenant, () => ctx.db.get(command.id))
136
+ if (row.state !== 'draft') errors.throw({ code: 'INVALID_STATE' })
137
+ },
138
+ audit: ({ command }) => ({ ... }),
139
+ }),
140
+ } as const
141
+
142
+ const commands = new Command<MutationCtx, typeof operations>(operations, {
143
+ guard: (ctx) => assertNotReadonlyWindow(ctx), // registry-wide default
144
+ })
145
+ ```
146
+
147
+ Guards deny by throwing; a denial before the handler never needs rollback
148
+ because nothing has executed. Keep guards read-only — a guard that writes is
149
+ a handler in disguise.
150
+
151
+ If any step throws, the Convex transaction rolls back — handler writes and
152
+ audit entry together. Audit and effects can never disagree.
153
+
154
+ ### Dynamic dispatch
155
+
156
+ `exec` has a second overload for operation selection at runtime:
157
+
158
+ ```ts
159
+ export const executeAny = commands.exec({
160
+ dispatcher: z.object({ kind: z.enum(OPERATION_KINDS) /* from constants.ts */ }),
161
+ select: (input) => `documents.${input.kind}` as const,
162
+ handler,
163
+ })
164
+ ```
165
+
166
+ An unconfigured operation throws `COMMAND_OPERATION_NOT_CONFIGURED`.
167
+
168
+ ## Observability — payload-free by construction
169
+
170
+ Observations carry identifiers and a duration, never payloads. Events are
171
+ silently dropped unless:
172
+
173
+ - `enabled` resolves to `true`,
174
+ - `operation` and `classification` match `/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/`,
175
+ - `errorCode` (when present) matches `/^[A-Z][A-Z0-9_]{0,95}$/`.
176
+
177
+ Practical consequence: **dotted lowercase operation names**
178
+ (`documents.rename`) and **UPPER_SNAKE error codes** are effectively
179
+ mandatory. Every telemetry path is wrapped in try/catch — observability is
180
+ behaviorally inert and can never change command behavior or mask the original
181
+ failure. Default sink is one JSON line on `console.info`
182
+ (`event: 'command.execution'`); inject `emit` to redirect.
183
+
184
+ ## `executeResultBoundary` — typed failures, but only while it's safe
185
+
186
+ ```ts
187
+ import { executeResultBoundary } from 'cvx-kit/components/foundation'
188
+
189
+ const result = await executeResultBoundary(ctx, () => run(), boundary)
190
+ // Result<Value, Failure> = { ok: true, value } | { ok: false, error }
191
+ ```
192
+
193
+ Semantics — read carefully, they are decided by **transaction state**, not by
194
+ the error:
195
+
196
+ - Success → `{ ok: true, value }`.
197
+ - A failure the boundary recognizes (`dataOf(error)` non-undefined) is
198
+ returned as `{ ok: false, error }` **only if the transaction has produced no
199
+ effects yet** (nothing written, nothing scheduled — checked via
200
+ `ctx.meta.getTransactionMetrics()`).
201
+ - If anything was already written or scheduled, the error is **rethrown** so
202
+ Convex rolls the transaction back — a returned failure after effects would
203
+ commit those effects.
204
+ - Unrecognized errors and unreadable metrics always rethrow.
205
+
206
+ Use it at the outermost edge of a mutation when the client needs a typed
207
+ `Result` over the wire instead of a thrown error. `projectResult(result,
208
+ onFailure)` unwraps on the caller side.
209
+
210
+ ## The `Query` kernel
211
+
212
+ `Query` mirrors the pattern for reads: an injected `execute` receives
213
+ `{ context, metadata, run }`, letting the host apply uniform read policy
214
+ (authorization checks, tracing) around every domain query without the domains
215
+ knowing. Metadata defaults are set at construction and merged per-executor.
216
+
217
+ ## Rules and footguns
218
+
219
+ 1. One `new Foundation(...)` per app, in `convex/foundation.ts`. Everything
220
+ else destructures from it.
221
+ 2. Never deep-import the component's `modules/*` or `result.ts` paths from app
222
+ code — the `client.ts` facade is the contract.
223
+ 3. The audit callback receives the ctx untyped (`never`); wiring a query ctx
224
+ into an audited command fails at runtime, not compile time. Commands run in
225
+ mutations.
226
+ 4. Name operations `domain.verb` and error codes `UPPER_SNAKE` or your
227
+ telemetry silently disappears (see regexes above).
228
+ 5. Keep operation registries frozen (`as const`) in the domain's
229
+ `commands.ts`; vocabulary tuples live in `constants.ts`.
@@ -0,0 +1,291 @@
1
+ # cvx-kit conventions — folder structure, file anatomy, and naming
2
+
3
+ This is the kit's doctrine. Not a style suggestion: the structure below is
4
+ what the kit's structural guarantees (auth, triggers, bounded reads, audited
5
+ commands) assume, and what its recommended architecture tests enforce. Follow
6
+ it as written; deviate only with a documented reason and a matching change to
7
+ the tests.
8
+
9
+ Names in `<angle brackets>` are placeholders for your vocabulary. Everything
10
+ else — the folder roles, the file anatomy, the naming grammar — is fixed.
11
+
12
+ ---
13
+
14
+ ## 1. Folder structure
15
+
16
+ ```
17
+ convex/
18
+ convex.config.ts
19
+ schema.ts
20
+ functions.ts
21
+ triggers.ts
22
+ foundation.ts
23
+ <component>.ts (one per mounted component: audit.ts, approvals.ts, ...)
24
+ auth.ts auth.config.ts http.ts crons.ts
25
+ api/
26
+ <entity>.ts
27
+ domain/
28
+ table.ts
29
+ shared/
30
+ <provider>.ts
31
+ <entity>/
32
+ schema.ts table.ts constants.ts commands.ts queries.ts
33
+ rules.ts shared.ts actions.ts
34
+ <workflow>.ts <workflow>_functions.ts
35
+ __tests__/
36
+ components/ (only if you author portable components)
37
+ <name>/
38
+ __tests__/
39
+ setup.ts architecture.test.ts facades.test.ts
40
+ ```
41
+
42
+ Three zones, three privileges:
43
+
44
+ - **Root** — configuration and facades. May import kit modules, generated
45
+ code, and component clients. May NOT export public functions or contain
46
+ business decisions.
47
+ - **`api/`** — the public surface. May import `functions.ts` constructors and
48
+ its entity's domain modules. May NOT contain business logic, private
49
+ helpers, or reads/writes beyond delegation.
50
+ - **`domain/`** — all business logic. May import `functions.ts`,
51
+ `foundation.ts`, facades, `domain/shared/`, and its own entity directory.
52
+ May NOT import `api/`, sibling entities, `_generated/server` builders, or
53
+ component internals.
54
+
55
+ Dependency direction is one-way: `api → domain → shared → kit`. Anything
56
+ pointing the other way is a violation.
57
+
58
+ ### Module grain
59
+
60
+ `domain/<entity>/` is really `domain/<module>/`: the directory is a **bounded
61
+ context that may own several related tables**, not necessarily one. Two
62
+ tables belong in the same module when a single command routinely writes both,
63
+ or when one's vocabulary is meaningless without the other; otherwise they are
64
+ separate modules. The file anatomy in §2 is unchanged — a module's
65
+ `schema.ts` simply declares several `zodTable`s and its `table.ts` exports
66
+ one merged map (combine maps in `domain/table.ts` with `createModule`, which
67
+ rejects duplicate owners). A one-table module is the degenerate case, so
68
+ small apps look exactly like before.
69
+
70
+ Cross-module rules:
71
+
72
+ - Sibling modules may import each other's `rules.ts` and `queries.ts`
73
+ **only** — never `commands.ts`: one module never mutates another's state.
74
+ - A workflow that must atomically write across modules lives in an optional
75
+ `application/` layer above `domain/` (`application/<workflow>.ts`); `api/`
76
+ adapters then import `application/*` and their own module, never a second
77
+ module.
78
+
79
+ ---
80
+
81
+ ## 2. File anatomy — what each file contains, exactly
82
+
83
+ Every file has one job. The lists below are exhaustive: if content isn't
84
+ listed for a file, it doesn't belong there.
85
+
86
+ ### Universal file order
87
+
88
+ Every source file reads from public purpose to private implementation:
89
+
90
+ 1. Imports.
91
+ 2. Exported API — types, schemas, constants, functions.
92
+ 3. File-local types and constants supporting the implementation.
93
+ 4. Private helpers as **named function declarations**, at the bottom.
94
+
95
+ No helper implementations above the exported API. No helper
96
+ arrow-function constants where a `function` declaration works. No barrel
97
+ files (`index.ts` re-exports) anywhere inside `convex/`. No re-exporting
98
+ another file's function to shorten an import.
99
+
100
+ ### Root files
101
+
102
+ | File | Contains | Never contains |
103
+ |---|---|---|
104
+ | `convex.config.ts` | `defineApp()`, one `app.use(...)` per component, `defineApp({ env })` declarations, `export default app` | anything else |
105
+ | `schema.ts` | `defineSchema(domainTables)` | table definitions, validators |
106
+ | `functions.ts` | the single `createAuthFunctions<DataModel>()` call and its exported constructors | handlers, business policy beyond the injected config |
107
+ | `triggers.ts` | `createTriggers()`, `timestamps`/`appendOnly`/`noDelete` registrations, calls to per-entity `register<Entity>Triggers` | trigger *logic* for a specific entity (that lives in the entity) |
108
+ | `foundation.ts` | the single `new Foundation(...)`, destructured exports | command definitions |
109
+ | `<component>.ts` | `new <Client>(components.<name>)` + minimal admin plumbing | workflow/business definitions |
110
+ | `http.ts` | routes: verify signature → parse payload → delegate to a domain function | webhook business logic |
111
+ | `crons.ts` | `cronJobs()` declarations targeting `internal.domain.<entity>...` | handler logic |
112
+
113
+ ### `api/<entity>.ts` — the adapter file
114
+
115
+ One file per entity exposed to clients. Each exported function follows one
116
+ template and stays within ~10 lines of glue:
117
+
118
+ ```ts
119
+ export const <verb> = authMutation({
120
+ args: <entity>.commandInput.extend({ /* ids, etc. */ }),
121
+ returns: <resultSchema>,
122
+ handler: (ctx, args) => execute<Verb><Entity>(ctx, { ...args, actorId: ctx.actor.userId }),
123
+ })
124
+
125
+ export const list = authQuery({
126
+ args: { limit: z.number() },
127
+ returns: z.array(<entity>.publicDto),
128
+ handler: (ctx, { limit }) =>
129
+ ctx.include(ctx.db.query('<entities>'))
130
+ .matching('by_<field>', (ix) => ix.eq('<field>', ctx.actor.userId))
131
+ .execute(limit, (rows) => rows.map(<entity>.toPublicDto)),
132
+ })
133
+ ```
134
+
135
+ Adapters do four things — parse (boundary schemas), authorize (constructor
136
+ choice), delegate (`execute*` with `ctx.actor`), project (`toPublicDto`).
137
+ The moment an adapter needs a private helper or a business `if`, that code
138
+ moves to the domain.
139
+
140
+ ### `domain/<entity>/schema.ts` — shapes only
141
+
142
+ - The entity's single `zodTable(...)` call, with `serverFields`,
143
+ `commandFields`, `publicFields` decided deliberately.
144
+ - DTO compositions derived from the table's boundaries
145
+ (`z.object({ ...<entity>.publicDto.shape, ... }).strict()`).
146
+ - Inferred types (`export type <Entity> = z.infer<typeof <entity>.schema>`).
147
+ - NO indexes, NO `defineTable`, NO enum literals (consume `constants.ts`).
148
+
149
+ ### `domain/<entity>/table.ts` — topology only
150
+
151
+ ```ts
152
+ export const <entity>Tables = {
153
+ <entities>: <entity>.table
154
+ .index('by_<field>', ['<field>'])
155
+ .index('by_<field>_and_<field2>', ['<field>', '<field2>']),
156
+ }
157
+ ```
158
+
159
+ Registered in `domain/table.ts`, which merges all entity maps into
160
+ `domainTables`. Shapes change with the data model; indexes change with query
161
+ patterns — that's why they are separate files.
162
+
163
+ ### `domain/<entity>/constants.ts` — vocabularies
164
+
165
+ Every finite value set the entity owns, one owner per vocabulary:
166
+
167
+ ```ts
168
+ export const <ENTITY>_STATES = ['<a>', '<b>', '<c>'] as const
169
+ export type <Entity>State = (typeof <ENTITY>_STATES)[number]
170
+ ```
171
+
172
+ Schemas, validators, filters, transition maps, and tests consume the tuple.
173
+ Transition maps are typed against the value type so an invalid literal cannot
174
+ compile. Numeric limits and durations the entity owns also live here.
175
+
176
+ ### `domain/<entity>/commands.ts` — every state change
177
+
178
+ 1. The frozen operation registry: `Command.operation({ command, result,
179
+ classification, audit })` per operation, keys `'<entity>.<verb>'`.
180
+ 2. One `new Command<Ctx, typeof operations>(operations)`.
181
+ 3. Exported executors: `export const execute<Verb><Entity> = commands.exec(...)`.
182
+ 4. Private handler helpers at the bottom.
183
+
184
+ Audit derivation lives here, next to the operation it describes. No reads
185
+ that belong in `queries.ts`; no pure predicates that belong in `rules.ts`.
186
+
187
+ ### `domain/<entity>/queries.ts` — domain reads
188
+
189
+ Read logic worth reusing beyond one adapter: multi-step lookups, DTO
190
+ assembly, cross-index selection. Bounded like everything else.
191
+
192
+ ### `domain/<entity>/rules.ts` — pure invariants
193
+
194
+ Predicates and transition checks with **no ctx and no db**: unit-testable
195
+ business truth (`can<Verb>(doc, actor)`, `isTransitionLegal(from, to)`).
196
+ Handlers call rules; rules never call handlers.
197
+
198
+ ### `domain/<entity>/shared.ts` — entity-private helpers
199
+
200
+ Code shared by two or more files *of this entity*. If a second entity needs
201
+ it, it moves to `domain/shared/` — never a sibling import.
202
+
203
+ ### `domain/<entity>/actions.ts` — external side effects
204
+
205
+ Action-side logic using the shared provider clients from
206
+ `domain/shared/<provider>.ts`. Domains import the shared client; they never
207
+ instantiate their own vendor SDK.
208
+
209
+ ### `domain/<entity>/<workflow>.ts` + `<workflow>_functions.ts`
210
+
211
+ The workflow/approval definition the entity owns, and its internal callback
212
+ targets built with `system*` constructors. Callbacks re-validate everything:
213
+ `normalizeId`, `resourceType` check, staleness guard.
214
+
215
+ ### `domain/shared/` — cross-entity, and only cross-entity
216
+
217
+ One configured client per external provider (`<provider>.ts`), plus zod/util
218
+ helpers genuinely used by multiple entities. This directory is not a dumping
219
+ ground: code with one consumer moves back to that consumer.
220
+
221
+ ### Tests
222
+
223
+ - `domain/<entity>/__tests__/` — behavioral tests for the entity, driven
224
+ through the real constructors (`t.withIdentity(...)`).
225
+ - `convex/__tests__/architecture.test.ts` — the executable conventions;
226
+ every rule proved against a synthetic violating fixture first.
227
+ - `convex/__tests__/facades.test.ts` — every `app.use` exactly once; no
228
+ private-child access.
229
+ - `convex/__tests__/setup.ts` — env stubs for `defineApp({ env })` vars.
230
+ - Test files are named `<subject>.test.ts` and live in a `__tests__/`
231
+ directory beside the code they test — never in a parallel top-level tree.
232
+
233
+ ---
234
+
235
+ ## 3. Naming grammar
236
+
237
+ | Thing | Convention | Example shape |
238
+ |---|---|---|
239
+ | Entity directory | plural camelCase, matches table name | `domain/<entities>/` |
240
+ | Table | plural camelCase | `<entities>` |
241
+ | Fields | camelCase; foreign keys `<other>Id`; timestamps only via the kit | `ownerId`, `archivedAt` |
242
+ | Index | `by_<field>` / `by_<field>_and_<field>`, declaration order, nested paths flattened with `_` | `by_ownerId_and_state_status` |
243
+ | Command operation | `<entity>.<verb>` lowercase dotted; must match `/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/` | `<entities>.approve` |
244
+ | Error code | `UPPER_SNAKE`, matching `/^[A-Z][A-Z0-9_]{0,95}$/`, defined in one owner | `INVALID_REQUEST_BOUNDARY` |
245
+ | Vocabulary tuple | `UPPER_SNAKE_CASE` `as const`; derived type PascalCase singular | `<ENTITY>_STATES` / `<Entity>State` |
246
+ | Domain executor | `execute<Verb><Entity>` | `executeApprove<Entity>` |
247
+ | Rule predicate | `can<Verb>` / `is<Condition>` | `canApprove`, `isTransitionLegal` |
248
+ | Trigger registration | `register<Entity>Triggers(triggers)` | exported from the entity, called in root `triggers.ts` |
249
+ | Internal-callback file | `<purpose>_functions.ts`, sibling of `<purpose>.ts` | `approval_functions.ts` |
250
+ | zodTable export | camelCase plural, same as table | `export const <entities> = zodTable(...)` |
251
+ | DTO schema | `<entity><Purpose>Dto` | `<entity>SummaryDto` |
252
+ | Zod schema value | camelCase noun | `commandInput`, `publicDto` |
253
+ | Type | PascalCase; no `I`/`T` prefixes | `<Entity>State` |
254
+ | Test file | `<subject>.test.ts` in `__tests__/` | `commands.test.ts` |
255
+ | Public function path | `api/<entity>:<fn>` — clients never call a root path | — |
256
+
257
+ The two regexes are enforced at runtime: observability silently drops events
258
+ whose operation/classification/errorCode don't match. The rest is enforced by
259
+ the architecture tests — a naming rule without a test is a wish.
260
+
261
+ ---
262
+
263
+ ## 4. Import rules
264
+
265
+ Allowed, per zone (anything not listed is forbidden):
266
+
267
+ | From | May import |
268
+ |---|---|
269
+ | `api/<entity>.ts` | `functions.ts`, `domain/<entity>/*`, `domain/shared/*`, zod |
270
+ | `domain/<entity>/*` | `functions.ts`, `foundation.ts`, component facades, `domain/shared/*`, own directory, generated `internal`/`api` (references only), kit modules |
271
+ | `domain/shared/*` | kit modules, vendor SDKs (this is the ONLY home for vendor clients) |
272
+ | root facades | kit modules, `components` from generated api |
273
+ | `functions.ts` | `_generated/server` (the only file allowed to) |
274
+ | `components/<name>/**` | its own directory + npm deps only — no host imports, no `process.env` |
275
+
276
+ Forbidden everywhere: sibling-entity imports, `api/` from `domain/`,
277
+ component internals (anything below a component's client facade),
278
+ `makeFunctionReference`, barrel files.
279
+
280
+ ---
281
+
282
+ ## 5. Formatting and language
283
+
284
+ - TypeScript strict; no `any` in exported signatures; `as never` only at kit
285
+ seams that document it.
286
+ - Zod objects that cross a boundary are `.strict()` — always.
287
+ - `Object.freeze` for registries and long-lived configuration objects.
288
+ - Comments state constraints the code can't (`// Optional only for rows
289
+ created before <epoch>.`) — never narration of the next line.
290
+ - One entity concept per file; when a file serves two purposes, split it
291
+ along the anatomy in §2.