cvx-kit 0.1.11 → 0.1.13

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 (46) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +1 -0
  3. package/dist/contracts.d.mts +2 -1
  4. package/dist/contracts.mjs +1 -1
  5. package/dist/index.d.mts +2 -1
  6. package/dist/index.mjs +1 -1
  7. package/dist/modules/contracts/binding.d.mts +26 -0
  8. package/dist/modules/contracts/binding.mjs +2 -0
  9. package/dist/modules/contracts/binding.mjs.map +1 -0
  10. package/dist/modules/contracts/domain.d.mts +59 -0
  11. package/dist/modules/contracts/domain.mjs +2 -0
  12. package/dist/modules/contracts/domain.mjs.map +1 -0
  13. package/dist/modules/effect/command.d.mts +39 -1
  14. package/dist/modules/effect/command.mjs +1 -1
  15. package/dist/modules/effect/command.mjs.map +1 -1
  16. package/dist/modules/effect/crud.d.mts +6 -0
  17. package/dist/modules/effect/domain.d.mts +72 -0
  18. package/dist/modules/effect/domain.mjs +2 -0
  19. package/dist/modules/effect/domain.mjs.map +1 -0
  20. package/dist/modules/effect/operation.d.mts +4 -3
  21. package/dist/modules/effect/operation.mjs.map +1 -1
  22. package/dist/modules/effect/query.d.mts +24 -8
  23. package/dist/modules/effect/query.mjs +1 -1
  24. package/dist/modules/effect/query.mjs.map +1 -1
  25. package/dist/modules/foundation/foundation.d.mts +109 -0
  26. package/dist/modules/foundation/foundation.mjs +2 -0
  27. package/dist/modules/foundation/foundation.mjs.map +1 -0
  28. package/dist/modules/foundation/operation.d.mts +140 -0
  29. package/dist/modules/foundation/operation.mjs +2 -0
  30. package/dist/modules/foundation/operation.mjs.map +1 -0
  31. package/dist/oxlint/ownership.mjs +1 -1
  32. package/dist/oxlint/ownership.mjs.map +1 -1
  33. package/dist/oxlint/reserved-files.d.mts +6 -0
  34. package/dist/oxlint/reserved-files.mjs +2 -0
  35. package/dist/oxlint/reserved-files.mjs.map +1 -0
  36. package/dist/oxlint.d.mts +1 -1
  37. package/dist/oxlint.mjs +1 -1
  38. package/dist/oxlint.mjs.map +1 -1
  39. package/docs/commands-legacy.md +3 -1
  40. package/docs/conventions.md +7 -3
  41. package/docs/domain-contracts.md +148 -0
  42. package/docs/effect.md +2 -0
  43. package/docs/oxlint.md +4 -0
  44. package/package.json +1 -1
  45. package/scripts/test-packed-effect.mjs +14 -0
  46. package/scripts/test-packed-oxlint.mjs +1 -1
@@ -0,0 +1,148 @@
1
+ # Optional domain contracts
2
+
3
+ Declare schemas and command classification once in `contracts.ts`, then attach
4
+ the declaration to a Command or Query registry. Errors belong at the root beside
5
+ commands and queries. Either section and the error contract may be omitted.
6
+
7
+ The regular registry uses `createFoundation` from `cvx-kit`; the Effect registry
8
+ uses `createEffectFoundation` from `cvx-kit/effect`. Both accept the same contract
9
+ and handler-first implementation helpers. Existing inline Effect definitions and the
10
+ older `new Foundation(...)` constructors continue to work.
11
+
12
+ ```ts
13
+ import { z } from 'zod'
14
+ import { createFoundation } from 'cvx-kit'
15
+ import { defineDomainContract } from 'cvx-kit/contracts'
16
+ import { defineErrorContract } from 'cvx-kit/errors'
17
+
18
+ const contract = defineDomainContract({
19
+ errors: defineErrorContract({ Missing: { message: 'Task missing', details: {} } }),
20
+ commands: {
21
+ 'tasks.create': {
22
+ input: z.string().transform(Number),
23
+ result: z.number().transform((count) => ({ count })),
24
+ classification: 'business',
25
+ },
26
+ 'tasks.close': {
27
+ input: z.object({ id: z.string() }),
28
+ result: z.boolean(),
29
+ classification: 'business',
30
+ },
31
+ },
32
+ queries: { 'tasks.get': { input: z.number(), result: z.number() } },
33
+ })
34
+ const { Command, Query } = createFoundation({
35
+ observability: {
36
+ enabled: false,
37
+ classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }),
38
+ },
39
+ writeAudit: (_host: { actorId: string }, entry) => {
40
+ console.log(entry.operation)
41
+ },
42
+ })
43
+ const commands = Command({
44
+ contract,
45
+ context: (host: { actorId: string }) => host,
46
+ audit: ({ operation, input, result }, context) => {
47
+ if (operation !== 'tasks.create') return null
48
+ return {
49
+ operation,
50
+ actorId: context.actorId,
51
+ aggregate: { type: 'task', id: String(input) },
52
+ metadata: { count: result.count },
53
+ }
54
+ },
55
+ operations: ({ command }) => ({
56
+ 'tasks.create': command.handler((input) => input + 1),
57
+ 'tasks.close': command.handler((input) => input.id.length > 0, { audit: () => null }),
58
+ }),
59
+ })
60
+ const queries = Query({
61
+ contract,
62
+ context: (host: { actorId: string }) => host,
63
+ operations: ({ query }) => ({ 'tasks.get': query.handler((input) => input) }),
64
+ })
65
+ const created = await commands.exec('tasks.create', '2', { actorId: 'actor' })
66
+ const count = await queries.withContext({ actorId: 'actor' }).exec('tasks.get', created.count)
67
+ void count
68
+ ```
69
+
70
+ Each map must implement exactly its declared keys, including maps supplied through
71
+ variables. The object key selects the contract entry and infers the handler and
72
+ option callback types through `command.handler(fn, options?)` or
73
+ `query.handler(fn, options?)`. Keep the map directly in `operations` for contextual
74
+ inference. For separately assembled maps, the existing named helpers provide explicit
75
+ contract selection without relying on the enclosing object for inference. Input, result,
76
+ classification, replayResult, and wire come from the declaration and cannot be
77
+ overridden in the implementation. Construction snapshots operation records and
78
+ map membership without cloning validators or running user callbacks.
79
+
80
+ Options include guard, middleware, metadata, and permission; commands also allow
81
+ prepare, aggregates, and audit overrides. Options are optional when a root audit
82
+ is configured; commands without one require an audit in their options. Existing
83
+ `command['tasks.create']({ handler, ...options })` and named query helpers remain
84
+ supported. Named helpers retain their exact key binding.
85
+
86
+ The handler receives decoded input, returns the result schema's accepted input,
87
+ and execution returns its decoded result. Normal handlers and hooks return values
88
+ or Promises. Effect handlers and hooks may also return Effects; their error and
89
+ service channels remain part of the selected operation's type. For example, with
90
+ `Command` from `createEffectFoundation`:
91
+
92
+ ```ts
93
+ import { Effect } from 'effect'
94
+ import { createEffectFoundation } from 'cvx-kit/effect'
95
+
96
+ const effectFoundation = createEffectFoundation({
97
+ observability: {
98
+ enabled: false,
99
+ classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }),
100
+ },
101
+ writeAudit: (_host: { actorId: string }) => undefined,
102
+ })
103
+ const effectCommands = effectFoundation.Command({
104
+ contract,
105
+ context: (host: { actorId: string }) => host,
106
+ audit: () => null,
107
+ operations: ({ command }) => ({
108
+ 'tasks.create': command.handler((input) => Effect.succeed(input + 1)),
109
+ 'tasks.close': command.handler((input) => Effect.succeed(input.id.length > 0)),
110
+ }),
111
+ })
112
+ ```
113
+
114
+ The same contract owns both implementations; selecting a different adapter does
115
+ not require repeating schemas.
116
+
117
+ For independent wire projections, use the existing `operationContract` helper
118
+ inside a domain declaration. It infers the projector's parameter from the result
119
+ schema; provide both its replayResult and wire fields. A command may instead
120
+ supply only replayResult directly in its domain entry. A query cannot prepare,
121
+ audit, declare aggregates, or declare replayResult.
122
+
123
+ A root audit receives `{ operation, input, result }` and resolved base context.
124
+ Its operation discriminates the corresponding input and result types. An
125
+ operation's audit override receives the existing `{ command, result }` envelope
126
+ and replaces the root audit. Exactly one callback runs after fresh-result
127
+ validation. Returning null skips the write. Replay skips handlers, audits,
128
+ writes, and completion; permission still runs before replay. Each command needs
129
+ a definite audit source. A definite override excludes the root audit's Effect
130
+ channels; an optional override retains channels from both possible callbacks.
131
+
132
+ Inline registries can also use root audit, leaving schemas and classification
133
+ on their operation definitions. The legacy constructor API keeps its existing
134
+ context-first execution signatures; the new regular registry uses input-first
135
+ handlers and `exec(operation, input, host)` like Effect.
136
+
137
+ Pass `contract.errors` explicitly to the existing `createEffectApi({ errors })`
138
+ option. Attaching a contract does not change a globally configured API. The
139
+ original error-contract instance still owns projection: foreign and forged errors
140
+ become UnknownFailure. Wire exposure remains explicit through `.expose(owner, key)`.
141
+
142
+ Enable `cvx/reserved-domain-file-anatomy` together with
143
+ `cvx/domain-file-responsibilities` to enforce reserved filenames. A domain's
144
+ commands.ts and queries.ts each export one genuine bound registry. Contracts are
145
+ declarative; schema.ts may declare several zodTable shapes; table.ts owns their
146
+ topology, and domain/table.ts uses createModule to assemble module maps. The rules
147
+ recognize aliases, namespaces, and the root foundation.ts facade. They do not
148
+ require absent reserved files to be created.
package/docs/effect.md CHANGED
@@ -399,3 +399,5 @@ Optional request context can also be added through the existing Effect API adapt
399
399
  and consume `ctx.commands.exec(name, input)` through the application's existing
400
400
  function helper names. Keep dependencies local to each feature; explicit execution
401
401
  and input-only command handlers remain supported. See [optional context injection](./command-context.md).
402
+
403
+ See [Optional domain contracts](./domain-contracts.md) for shared declarations, named implementations, and registry audit defaults in both Effect and Promise registries.
package/docs/oxlint.md CHANGED
@@ -144,3 +144,7 @@ Shared ownership follows static runtime imports through shared helpers. A shared
144
144
  file reached from only one domain is reported for relocation. Unused code,
145
145
  type-only consumers, and computed dependencies are not treated as proof of shared
146
146
  ownership; their ownership still needs review.
147
+
148
+ ## Reserved domain files
149
+
150
+ Enable `cvx/reserved-domain-file-anatomy` to require exported Command/Query registries, declarative contracts, zodTable shapes, and table topology or createModule assembly. Constructor provenance is traced through imported aliases, namespaces, direct Foundation factories, and the root foundation.ts facade. Pair it with `cvx/domain-file-responsibilities` to reject handlers and database/runtime work in contracts.ts. Backend scope and test/generated exclusions apply; absent files are not required.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvx-kit",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
5
5
  "keywords": [
6
6
  "component",
@@ -29,6 +29,7 @@ const extraTypeFixtures = [
29
29
  join(root, 'README.md'),
30
30
  join(root, 'src/docs/effect.md'),
31
31
  join(root, 'src/docs/commands.md'),
32
+ join(root, 'src/docs/domain-contracts.md'),
32
33
  join(root, 'src/docs/command-context.md'),
33
34
  join(root, 'src/docs/commands-legacy.md'),
34
35
  join(root, 'src/docs/auth.md'),
@@ -54,6 +55,7 @@ function run(command, args, cwd) {
54
55
  function copyPublicTypeFixture(fixture, filename, publicImport) {
55
56
  const source = readFileSync(join(root, 'test', filename), 'utf8')
56
57
  .replaceAll(publicImport, 'cvx-kit/effect')
58
+ .replaceAll('../src/index', 'cvx-kit')
57
59
  .replaceAll('../src/errors', 'cvx-kit/errors')
58
60
  .replaceAll('../src/contracts', 'cvx-kit/contracts')
59
61
  .replaceAll('../src/idempotency', 'cvx-kit/idempotency')
@@ -126,6 +128,8 @@ function copyExtraTypeFixtures(fixture, paths) {
126
128
  }
127
129
 
128
130
  const legacyRuntime = `
131
+ import { createFoundation } from 'cvx-kit'
132
+ import { defineDomainContract } from 'cvx-kit/contracts'
129
133
  import { strict as assert } from 'node:assert'
130
134
  import { createRequire } from 'node:module'
131
135
  import { Foundation } from 'cvx-kit/components/foundation'
@@ -210,6 +214,10 @@ assert.equal(replay.kind, 'replay')
210
214
  assert.deepEqual(await decodeContract(idempotency.replayResult, replay.result), { value: 'final!' })
211
215
  assert.equal(authorizations, 2)
212
216
  assert.equal(ledger.length, 1)
217
+ const domain = defineDomainContract({ commands: { save: { input: z.string().transform(Number), result: z.number().transform(count => ({ count })), classification: 'business' } } })
218
+ const regular = createFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => undefined })
219
+ const domainCommands = regular.Command({ contract: domain, context: host => host, audit: () => null, operations: ({ command }) => ({ save: command.save({ handler: value => value + 1 }) }) })
220
+ assert.deepEqual(await domainCommands.exec('save', '2', {}), { count: 3 })
213
221
  `
214
222
 
215
223
  const legacyTypes = `
@@ -387,6 +395,9 @@ try {
387
395
  writeFileSync(join(fixture, 'legacy-types.ts'), legacyTypes)
388
396
  const typeFiles = [
389
397
  'legacy-types.ts',
398
+ copyPublicTypeFixture(fixture, 'domain-contract-types.ts', '../src/effect'),
399
+ copyPublicTypeFixture(fixture, 'domain-foundation-types.ts', '../src/effect'),
400
+ copyPublicTypeFixture(fixture, 'domain-contract-guide-types.ts', '../src/effect'),
390
401
  copyPublicTypeFixture(fixture, 'operation-neutral-types.ts', '../src/effect'),
391
402
  copyPublicTypeFixture(fixture, 'operation-agent-tools-types.ts', '../src/effect'),
392
403
  ]
@@ -394,6 +405,9 @@ try {
394
405
  if (withEffect) {
395
406
  for (const filename of [
396
407
  'effect-foundation-types.ts',
408
+ 'domain-effect-types.ts',
409
+ 'domain-handler-types.ts',
410
+ 'domain-exactness-types.ts',
397
411
  'effect-api-types.ts',
398
412
  'effect-api-zod-types.ts',
399
413
  'effect-auth-types.ts',
@@ -9,7 +9,7 @@ const root = join(import.meta.dirname, '..')
9
9
  const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-oxlint-'))
10
10
  const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
11
11
  const ruleNames = Object.keys(rules)
12
- assert.equal(ruleNames.length, 20)
12
+ assert.equal(ruleNames.length, 21)
13
13
 
14
14
  try {
15
15
  run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)