cvx-kit 0.1.14 → 0.1.16

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.
Files changed (51) hide show
  1. package/README.md +64 -45
  2. package/dist/querying/cursors.d.mts +5 -0
  3. package/dist/querying/cursors.mjs +1 -1
  4. package/dist/querying/cursors.mjs.map +1 -1
  5. package/dist/querying.d.mts +20 -6
  6. package/dist/querying.mjs +1 -1
  7. package/dist/querying.mjs.map +1 -1
  8. package/dist/relations.mjs +1 -1
  9. package/dist/relations.mjs.map +1 -1
  10. package/dist/zod-table.d.mts +59 -2
  11. package/dist/zod-table.mjs +1 -1
  12. package/dist/zod-table.mjs.map +1 -1
  13. package/package.json +10 -3
  14. package/AGENTS.md +0 -47
  15. package/CHANGELOG.md +0 -324
  16. package/ROADMAP.md +0 -126
  17. package/docs/agent-tools.md +0 -41
  18. package/docs/approvals.md +0 -201
  19. package/docs/architecture.md +0 -124
  20. package/docs/auth.md +0 -443
  21. package/docs/command-context.md +0 -150
  22. package/docs/commands-legacy.md +0 -513
  23. package/docs/commands.md +0 -173
  24. package/docs/conventions.md +0 -345
  25. package/docs/crud.md +0 -66
  26. package/docs/domain-contracts.md +0 -148
  27. package/docs/domain-operations.md +0 -615
  28. package/docs/effect.md +0 -403
  29. package/docs/llms.md +0 -324
  30. package/docs/maintainability.md +0 -124
  31. package/docs/migration.md +0 -351
  32. package/docs/oxlint.md +0 -254
  33. package/docs/querying.md +0 -293
  34. package/docs/tenancy.md +0 -176
  35. package/docs/triggers.md +0 -77
  36. package/docs/upgrading.md +0 -287
  37. package/docs/webhooks.md +0 -58
  38. package/docs/zod-table.md +0 -278
  39. package/scripts/convex-config-bridge.mjs +0 -44
  40. package/scripts/convex-smoke-env.mjs +0 -33
  41. package/scripts/extract-changelog.mjs +0 -29
  42. package/scripts/owned-process.mjs +0 -140
  43. package/scripts/owned-process.test.mjs +0 -107
  44. package/scripts/smoke-packed-components.mjs +0 -341
  45. package/scripts/smoke-packed-effect.mjs +0 -439
  46. package/scripts/smoke-packed-querying.mjs +0 -296
  47. package/scripts/test-packed-auth.mjs +0 -72
  48. package/scripts/test-packed-effect.mjs +0 -482
  49. package/scripts/test-packed-ids.mjs +0 -150
  50. package/scripts/test-packed-oxlint.mjs +0 -167
  51. package/scripts/test-packed-querying.mjs +0 -185
package/ROADMAP.md DELETED
@@ -1,126 +0,0 @@
1
- # Roadmap to 1.0.0
2
-
3
- Where cvx-kit is going and what "stable" will mean. Tracked with checkboxes;
4
- each milestone ships as one minor version. From `0.1.0` on, **breaking
5
- changes only land on minor bumps** (0.x semver: minor = breaking, patch =
6
- fix/additive) and every release gets a CHANGELOG entry.
7
-
8
- ## 0.1.0 — the big one: API consolidation + the feature set
9
-
10
- Goal: one release that both closes the breaking window AND ships the full
11
- helper surface, so consumers adopt a complete kit once instead of chasing
12
- minors. Everything here may still rename freely — that's why the features
13
- belong in this milestone: they land already conforming to the frozen names.
14
-
15
- ### Consolidation (the last breaking pass)
16
-
17
- - [ ] Decide the published name (`cvx-kit` is a working title) and, if it
18
- changes, publish under the final name with `cvx-kit` deprecated.
19
- - [ ] API audit: review every exported name, option key, and error code
20
- against `conventions.md`'s own naming grammar; freeze the error-code
21
- taxonomy (`KitError` codes) as a documented tuple.
22
- - [x] Typed middleware context accumulation: `next({ context })` currently
23
- merges loosely; tighten so enrichment widens the downstream ctx type
24
- (TanStack-style), without breaking the loose form.
25
- - [x] Introduce `CHANGELOG.md` + release notes on the GitHub tag; adopt the
26
- 0.x semver contract above. (GitHub Releases automated in release.yml.)
27
- - [ ] CI hardening: add `--dry-run` pack + `attw` (Are The Types Wrong) next
28
- to publint; bump the deprecated actions pins.
29
-
30
- ### Features
31
-
32
- - [x] **Pagination bridge** — `paginated(table.publicDto)` arg/return
33
- helpers over Convex's pagination validators, and an
34
- `include(...).paginate(opts)` terminal next to `.execute(limit)`:
35
- cursors, not just bounded `take`.
36
- - [x] **CRUD command factory** — `createCrudCommands(table, policy)`
37
- generating the standard create/update/archive operations from a
38
- zodTable's masks: command input from `commandFields`, audit derivation
39
- from the aggregate, timestamps/tenant already structural. The single
40
- biggest remaining per-entity boilerplate.
41
- - [x] **State-machine helper** — `createStateMachine(STATES, transitions)`
42
- typed against the constants tuple, returning `isTransitionLegal` /
43
- `assertTransition` that drop straight into a command `guard`.
44
- - [x] **Rate-limit middleware factory** — the first official middleware:
45
- `rateLimit({ key: (ctx) => ctx.tenant, ... })` over
46
- `@convex-dev/rate-limiter`, packaged as `Command.middleware` /
47
- `Query.middleware` and the template for future middleware.
48
- - [x] **Webhook boundary helper** — signature verification + event dedup by
49
- natural key (`${event}:${id}:${updatedAt}`) + delegation to a
50
- `system*` function: the one boundary that genuinely needs dedup, as a
51
- first-class helper.
52
- - [x] **Agent tool generator** — `createAgentTools(table, executors)`
53
- emitting `@convex-dev/agent` tool definitions from the existing
54
- `tools.*` masks + `jsonSafeZid`, with handlers routed through the
55
- command protocol — agents get audited commands, never raw db access.
56
-
57
- Each feature ships with its own doc page (or section), tests on the real
58
- Convex runtime where applicable, and an `llms.md` entry.
59
-
60
- ## 0.2.0 — guardrails as a product
61
-
62
- Goal: the conventions the docs prescribe become importable tests, so a
63
- consumer gets the origin app's discipline on day one — enforced against the
64
- by-then-frozen 0.1.0 names.
65
-
66
- - [ ] `createArchitectureTest(rules)` — glob-based rule runner with the
67
- standard rules built in (no raw builders outside functions.ts, no
68
- inline tables/enums, no sibling-domain imports, no deep component
69
- imports, facades mount once), each rule fixture-proved, extensible
70
- with app-specific rules.
71
- - [ ] Tenancy exhaustiveness helper: assert every `tenantTable` is in the
72
- registry and vice versa; assert internal builders accept no tenant arg.
73
- - [ ] `llms.md` conformance check: an automated test that every export in
74
- its tables exists with that signature (also an rc gate — build it here).
75
- - [ ] Port the approvals copyability test (cp -r + tsc) into this repo's
76
- suite as the permanent portability guarantee for both components.
77
-
78
- ## 0.3.0 — scaffolding
79
-
80
- Goal: the first hour of a new app is generated, not hand-written.
81
-
82
- - [ ] WorkOS scaffold generator: `auth.ts`, `auth.config.ts`, `http.ts`,
83
- `functions.ts`, `foundation.ts`, `triggers.ts` from one command.
84
- - [ ] Audit facade helpers over any `convex-audit-log` instance:
85
- fingerprinted cursors, bounded windows, admin readers.
86
- - [ ] Decide on the optional idempotency/replay component (deferred from
87
- 0.0.x — only if a consumer demonstrates the need beyond Convex's
88
- exactly-once mutation delivery).
89
-
90
- ## 1.0.0-rc — proving it
91
-
92
- Entry gates, not work items. The rc ships when all are true:
93
-
94
- - [ ] Two real applications run the latest kit in production-like use for
95
- several weeks (the multi-tenant integration and one more).
96
- - [ ] One full minor cycle with zero breaking changes needed.
97
- - [ ] Docs complete and verified: the `llms.md` conformance check (built in
98
- 0.2.0) green in CI.
99
- - [ ] CI matrix: bun + node LTS, convex peer-range min and latest.
100
- - [ ] Public-API test coverage: every exported runtime symbol exercised by
101
- at least one test.
102
-
103
- ## 1.0.0
104
-
105
- - [ ] Freeze the export surface; document the semver contract (what counts
106
- as breaking, deprecation policy of one minor with warnings).
107
- - [ ] `MIGRATING.md` from 0.x.
108
- - [ ] Provenance, README badges, and a docs site or expanded README landing.
109
-
110
- ## Candidates (recorded, not committed)
111
-
112
- - Observability sink adapters: OTel/Axiom-shaped formatters for the
113
- injectable `emit`, so `command.execution` events land in real telemetry
114
- without per-consumer mapping.
115
- - Migration companion: a thin `@convex-dev/migrations` wrapper aligned with
116
- zodTable epochs ("make optional field required after backfill" as a
117
- declared step), closing the loop with `migration.md`.
118
-
119
- ## Explicitly out of scope for 1.0
120
-
121
- - General idempotency store (platform already covers single-call
122
- exactly-once; boundary dedup is the webhook helper's job).
123
- - Frontend helpers/React bindings.
124
- - Multi-provider auth abstractions beyond the injected-policy seam
125
- (`createAuthFunctions` already accepts any provider via `getAuthUser`/
126
- `mapRole`/`verifyMembership`).
@@ -1,41 +0,0 @@
1
- # `cvx-kit/agent-tools` — agents get audited commands
2
-
3
- `createAgentTools` turns a table's tool masks into framework-agnostic tool
4
- records `{ name, description, args, handler }` — directly spreadable into
5
- `@convex-dev/agent`'s `createTool`, adaptable to ai-sdk `tool()`, and the
6
- kit depends on neither (shape checked against `@convex-dev/agent@0.6.x`;
7
- re-verify at install).
8
-
9
- The point is the routing, not the shape: **mutation handlers go through
10
- command executors**, so every agent action is validated, guarded, audited,
11
- and observed. An agent never gets raw db access.
12
-
13
- ```ts
14
- import { createAgentTools } from 'cvx-kit/agent-tools'
15
-
16
- const tools = createAgentTools(<entities>, {
17
- // mutations — audited commands (createCrudCommands executors fit exactly):
18
- create: (ctx, input) => crud.executeCreate(ctx, input),
19
- update: (ctx, input) => crud.executeUpdate(ctx, input),
20
- archive: (ctx, input) => crud.executeArchive(ctx, input),
21
- // reads — caller-supplied query handlers (reads don't route through commands):
22
- get: (ctx, { id }) => loadDto(ctx, id),
23
- list: (ctx, { paginationOpts }) => listPage(ctx, paginationOpts),
24
- })
25
-
26
- // host-side, with @convex-dev/agent:
27
- // const agentTools = Object.fromEntries(Object.values(tools).map((t) =>
28
- // [t.name, createTool({ description: t.description, args: t.args, handler: t.handler })]))
29
- ```
30
-
31
- ## Contracts
32
-
33
- - **Args are the table masks, no drift**: `create` = `tools.insert`
34
- (`commandFields`), `update` = `tools.update`, `archive`/`get` = `tools.id`,
35
- `list` = the pagination opts boundary. Ids present as plain strings in
36
- generated JSON schemas (`jsonSafeZid`) while keeping `Id<...>` types.
37
- - **Only the kinds you supply handlers for are emitted**; names are
38
- `<tableName>_<verb>`; descriptions are derived and overridable via
39
- `options.descriptions`.
40
- - DTO discipline applies to reads: `get`/`list` handlers should return
41
- `toPublicDto`-projected data, same as any public query.
package/docs/approvals.md DELETED
@@ -1,201 +0,0 @@
1
- # `cvx-kit/components/approvals` — declarative approval workflows
2
-
3
- A self-contained Convex component for human-in-the-loop approval flows:
4
- declarative step lists (decision / branch / mutation / action / notify) with
5
- count quorum, maker-checker, expiry, compatibility keys, durable execution via
6
- a **nested** `@convex-dev/workflow`, and its own audit trail via a **nested**
7
- `convex-audit-log`. The component never imports host code: tenancy is an
8
- opaque `scopeRef`, resources are opaque `resourceType`/`resourceRef` strings,
9
- and callbacks cross the boundary as **function handles**.
10
-
11
- ## Mounting
12
-
13
- ```ts
14
- // convex/convex.config.ts
15
- import approvals from 'cvx-kit/components/approvals/convex.config'
16
- app.use(approvals)
17
- ```
18
-
19
- After installation or upgrade, deploy once and run the component health query
20
- from a host wrapper:
21
-
22
- ```ts
23
- return ctx.runQuery(components.approvals.health.check, {})
24
- ```
25
-
26
- It returns the installed approvals schema version and required index names. It
27
- also executes reads through
28
- `approvalDecisions.by_runId_and_decidedAt` and
29
- `approvalDecisions.by_runId_and_stepKey_and_actor_actorRef`, failing with an
30
- installation-specific diagnostic if the packaged component schema was not
31
- deployed.
32
-
33
- The component mounts its own private `workflow` and `auditLog` children. Do
34
- **not** re-register those children at the app root under the same identity,
35
- and never reach into `components.approvals.workflow` / `.auditLog` from host
36
- code — the `Approvals` client class is the only contract.
37
-
38
- ```ts
39
- // convex/approvals.ts — host facade, declared once
40
- import { Approvals } from 'cvx-kit/components/approvals'
41
- import { components } from './_generated/api'
42
-
43
- export const approvals = new Approvals(components.approvals)
44
- ```
45
-
46
- ## Defining a workflow — in the owning domain
47
-
48
- Workflow _definitions_ are business logic and live in the domain that owns the
49
- resource, not in the root facade:
50
-
51
- ```ts
52
- // convex/domain/documents/approval.ts
53
- import { approvals } from '../../approvals'
54
- import { internal } from '../../_generated/api'
55
-
56
- export const publishApproval = approvals.define({
57
- name: 'documentPublish',
58
- steps: [
59
- approvals.decision('managerDecision', {
60
- decisions: ['approved', 'rejected'],
61
- quorum: { kind: 'count', approvals: 1 },
62
- makerChecker: true, // requester cannot decide
63
- expiresAfterMs: 7 * 24 * 60 * 60 * 1000,
64
- }),
65
- approvals.branch('applyDecision', {
66
- approvedStepKey: 'publishDocument',
67
- rejectedStepKey: 'notifyRejected',
68
- }),
69
- approvals.mutation('publishDocument', {
70
- handler: internal.domain.documents.approval_functions.applyDecision,
71
- }),
72
- approvals.notify('notifyRejected', {
73
- handler: internal.domain.documents.approval_functions.notifyRejected,
74
- retry: true,
75
- }),
76
- ],
77
- })
78
- ```
79
-
80
- ### Step kinds
81
-
82
- | Step | Runs | Notes |
83
- | ---------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------- |
84
- | `decision(key, { decisions, quorum, makerChecker?, expiresAfterMs? })` | waits for humans | quorum is `{ kind: 'count', approvals: n }`; expiry resolves the run as `expired` |
85
- | `branch(key, { approvedStepKey, rejectedStepKey })` | routing | jumps by the preceding decision's outcome |
86
- | `mutation(key, { handler, retry? })` | internal mutation handle | transactional apply step |
87
- | `action(key, { handler, retry? })` | internal action handle | side effects (third parties) |
88
- | `notify(key, { handler, retry? })` | internal action handle | semantically a notification; same contract as action |
89
-
90
- Limits (enforced by the component's validators): ≤ 32 steps per workflow,
91
- ≤ 8 decisions per decision step, names ≤ 96 chars, references ≤ 256 chars,
92
- metadata ≤ 32 entries (keys ≤ 64, values ≤ 512 chars).
93
-
94
- ### Compatibility keys
95
-
96
- `compatibilityKey` (defaults to `name`) versions the workflow shape. Every
97
- `decide`/`cancel`/`status`/`restart` call sends it, so a run started under an
98
- old definition cannot be driven by an incompatible new one. When you change a
99
- workflow's steps in a breaking way, change the key.
100
-
101
- ## Driving a run
102
-
103
- ```ts
104
- // start — from an authMutation, with ctx.actor as the requester
105
- const { runId } = await publishApproval.start(ctx, {
106
- scopeRef: ctx.org.organizationId, // opaque tenancy
107
- resourceType: 'document',
108
- resourceRef: documentId, // opaque to the component
109
- requester: ctx.actor,
110
- metadata: { title }, // bounded string map
111
- })
112
-
113
- // decide — a manager approves or rejects
114
- await publishApproval.decide(ctx, { runId, decision: 'approved', reason }, ctx.actor)
115
-
116
- // observe from a host query
117
- await publishApproval.status(ctx, runId) // pending | approved | rejected | expired | canceled
118
- await publishApproval.evidence(ctx, runId) // per-step decision evidence
119
- await publishApproval.list(ctx, {
120
- scopeRef,
121
- state: 'pending',
122
- paginationOpts: { cursor: null, numItems: 20 },
123
- })
124
-
125
- // manage
126
- await publishApproval.cancel(ctx, runId, ctx.actor)
127
- await publishApproval.restart(ctx, runId)
128
- await approvals.cleanupAudit(ctx, { olderThanDays: 365, batchSize: 100 })
129
- ```
130
-
131
- ## Callback handlers — validate everything coming back
132
-
133
- Step handlers are **internal** functions (build them with `systemMutation` /
134
- `systemAction`). They receive `ApprovalCallbackInput`: `{ runId, scopeRef,
135
- resourceType, resourceRef, metadata?, decision? }` — all opaque strings.
136
- Because the component cannot know your tables, the handler must re-establish
137
- trust:
138
-
139
- ```ts
140
- import { approvalCallbackArgs } from 'cvx-kit/components/approvals'
141
-
142
- export const applyDecision = systemMutation({
143
- args: approvalCallbackArgs,
144
- handler: async (ctx, input) => {
145
- if (input.resourceType !== 'document') throw new Error('WRONG_RESOURCE')
146
- const id = ctx.db.normalizeId('documents', input.resourceRef)
147
- if (!id) throw new Error('BAD_REF')
148
- const document = await ctx.db.get(id)
149
- // guard against stale runs: the doc must still point at this run
150
- if (document?.approvalRunId !== input.runId) return
151
- await ctx.db.patch(id, { reviewState: 'published' })
152
- },
153
- })
154
- ```
155
-
156
- That `approvalRunId` check is the idempotency/staleness guard from the origin
157
- app — copy it.
158
-
159
- For branch callbacks, `decision.evidence` contains
160
- `{ stepKey, actorRef, decision, reason?, decidedAt }` items. Use the exported
161
- `approvalCallbackArgs` validator instead of duplicating that protocol boundary.
162
-
163
- ### Paginated inboxes
164
-
165
- Expose the configured workflow through a host query; component pagination uses
166
- `convex-helpers` internally because Convex's built-in `.paginate()` is not
167
- available inside components:
168
-
169
- ```ts
170
- export const approvalInbox = authQuery({
171
- args: {
172
- state: z.enum(['pending', 'approved', 'rejected', 'expired', 'canceled']).optional(),
173
- paginationOpts: convexToZod(paginationOptsValidator),
174
- },
175
- handler: (ctx, args) =>
176
- publishApproval.list(ctx, {
177
- scopeRef: ctx.org.organizationId,
178
- ...args,
179
- }),
180
- })
181
- ```
182
-
183
- The result keeps Convex's standard pagination shape, so React consumers can
184
- call `usePaginatedQuery(api.approvals.approvalInbox, { state: 'pending' },
185
- { initialNumItems: 20 })`. Pass each returned cursor only back to the same
186
- scope/state query.
187
-
188
- ## Rules and footguns
189
-
190
- 1. Root facade (`convex/approvals.ts`) instantiates the client; workflow
191
- _definitions_ live in the owning domain.
192
- 2. Handlers get opaque strings — always `normalizeId`, re-check
193
- `resourceType`, and verify the resource still references the run.
194
- 3. `makerChecker: true` means the requester's decisions are rejected — design
195
- your UI accordingly.
196
- 4. The component keeps its own audit trail (retention category
197
- `approval-protocol`, cleanup via `cleanupAudit`, retention 1–3650 days,
198
- batch ≤ 100). Host-side business audit still goes through your command
199
- protocol — they are different trails.
200
- 5. Changing steps without changing `compatibilityKey` lets old runs be driven
201
- by a new incompatible shape; bump the key on breaking changes.
@@ -1,124 +0,0 @@
1
- # Architecture — the shape of a cvx-kit application
2
-
3
- cvx-kit was extracted from a production Convex application (~12k LOC backend).
4
- This document describes the architecture that application follows and that the
5
- kit is designed to reproduce: strict ownership boundaries, injected policy,
6
- portable components, and conventions that are _executable_ (enforced by tests)
7
- rather than aspirational.
8
-
9
- ## The layers
10
-
11
- ```
12
- convex/
13
- convex.config.ts # component mounting — each app.use(x) exactly once
14
- schema.ts # defineSchema(domainTables) — nothing else
15
- functions.ts # createAuthFunctions(...) — the ONLY place raw builders appear
16
- foundation.ts # const { Command, Query, observability } = createEffectFoundation(...)
17
- approvals.ts # const approvals = new Approvals(components.approvals)
18
- audit.ts # writeAuditEntry over convex-audit-log + admin readers
19
- triggers.ts # the single Triggers registry + helper registrations
20
- auth.ts / auth.config.ts / http.ts # identity provider wiring (e.g. WorkOS AuthKit)
21
- api/ # PUBLIC surface — thin adapters only
22
- <documents>.ts # authQuery/authMutation wrapping domain executors
23
- domain/
24
- table.ts # merges per-domain table maps into domainTables
25
- shared/ # genuinely cross-entity helpers only
26
- <entity>/ # one directory per entity/aggregate
27
- schema.ts # zodTable declaration (shapes only)
28
- table.ts # indexes attached to <zodTable>.table
29
- constants.ts # finite vocabularies (UPPER_SNAKE readonly tuples)
30
- commands.ts # Command({ context, operations }) with handlers inside command({...})
31
- queries.ts # domain read logic
32
- approval.ts # approvals.define(...) workflow definitions
33
- *_functions.ts # internal callbacks (systemMutation/systemAction)
34
- ```
35
-
36
- ### Layer rules
37
-
38
- 1. **`api/` is the only public surface.** It contains no bare `query(` or
39
- `mutation(` — only `auth*`/`role*`/`admin*` constructors delegating to
40
- domain `execute*` functions. Public functions are adapters: parse, pass
41
- `ctx.actor`, project DTOs.
42
- 2. **`domain/<entity>/` owns everything about the entity** — its zod shape,
43
- indexes, vocabularies, commands, queries, workflows. Cross-domain reuse
44
- goes through `domain/shared/`, never sibling imports of another domain's
45
- internals.
46
- 3. **Root files are facades over mounted components.** `foundation.ts`,
47
- `approvals.ts`, `audit.ts` configure infrastructure; business definitions
48
- stay in the owning domain.
49
- 4. **Internal functions use `systemMutation`/`systemAction`** — never raw
50
- `internalMutation(`/`internalAction(` — so triggers and `include` apply to
51
- scheduled/callback work too.
52
-
53
- ## Injection, not import
54
-
55
- The kit's central design rule: **library code never imports app singletons**.
56
- Every kit surface takes its policy as a parameter:
57
-
58
- - `createAuthFunctions` receives the generated builders, `getAuthUser`,
59
- `mapRole`, `verifyMembership`, `triggers`, `errors`.
60
- - `createEffectFoundation` receives `classifyError`, `writeAudit`, the `enabled` flag.
61
- - `Approvals` receives only its component reference; host data crosses as
62
- opaque validated strings.
63
-
64
- The reverse rule holds for components: a portable component must not import
65
- host domains, generated host APIs, environment configuration, or vendor SDKs
66
- (WorkOS, Drizzle, Redis…). Host values cross its public API only as
67
- **validated, bounded, opaque data** — `scopeRef`, `resourceRef`, function
68
- handles. Component schemas contain only their own records and never mirror a
69
- private child component's tables.
70
-
71
- ## Boundaries as schemas
72
-
73
- Data crossing any boundary is a zod schema derived from one `zodTable`
74
- declaration per entity (see `zod-table.md`):
75
-
76
- - storage → `defineTable` via `zodToConvex`
77
- - writes → `insertSchema`/`updateSchema` (server fields and timestamps
78
- excluded)
79
- - commands → `commandInput` (explicit allowlist)
80
- - clients → `publicDto` + `toPublicDto` (runtime redaction)
81
- - LLM tools → `tools.*` and `jsonSafeZid`
82
-
83
- Everything is `.strict()`. There is exactly one source of truth per entity and
84
- no boundary that "just trusts" its input.
85
-
86
- ## Writes as commands
87
-
88
- State changes flow through the audited command protocol (see `commands.md`):
89
- parse input → observe → run handler → parse output → write audit — one
90
- transaction, rollback-consistent. Classifications and audit derivation are
91
- type-required per operation. Human-gated changes use the approvals component;
92
- its apply steps are commands too.
93
-
94
- ## Conventions worth enforcing with tests
95
-
96
- The origin app enforces its architecture with a test suite that raw-globs the
97
- source and asserts violations — each rule proved against a synthetic fixture
98
- first so the checker can't silently pass. Highly recommended for consumers:
99
-
100
- - **No inline tables/schemas** — `defineTable`/`defineSchema` only in
101
- `table.ts`/`schema.ts`.
102
- - **No inline enums** — `z.enum([...])` with an inline array literal is
103
- banned; vocabularies are named `UPPER_SNAKE` tuples in `constants.ts`.
104
- - **No deep server imports** — components consumed only through their
105
- `client.ts` facade; explicit allowlist for exceptions.
106
- - **No handwritten references** — `makeFunctionReference` banned; use
107
- generated `internal`/`api`.
108
- - **No sibling-domain imports** — a domain reaches another only via
109
- `domain/shared/` or the public API.
110
- - **Facades mount once** — every `app.use(x)` appears exactly once; facades
111
- never reach a component's private children.
112
- - **Component boundary tests** — no host imports, no
113
- `process.env`/vendor-SDK references inside `components/*`.
114
- - **Copyability** — copy the component to a tmpdir with only its package
115
- deps, run `tsc`: proves portability continuously.
116
-
117
- ## Component mounting order
118
-
119
- In `convex.config.ts`, mount infrastructure before consumers: auth kit →
120
- foundation → audit log → approvals → named workflow/workpool instances. Use
121
- `app.use(x, { name })` when mounting the same component twice; `components.
122
- <name>` keys off that name. Required env vars can be declared on
123
- `defineApp({ env: { ... } })` so deploys fail fast — remember to stub them in
124
- test setup.