cvx-kit 0.1.5 → 0.1.7

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 (124) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +21 -3
  3. package/dist/agent-tools.d.mts +2 -1
  4. package/dist/agent-tools.mjs +1 -1
  5. package/dist/agent-tools.mjs.map +1 -1
  6. package/dist/components/approvals/audit.mjs +1 -1
  7. package/dist/components/foundation/client.d.mts +8 -7
  8. package/dist/components/foundation/client.mjs +1 -1
  9. package/dist/components/foundation/client.mjs.map +1 -1
  10. package/dist/components/foundation/modules/command/command.d.mts +50 -10
  11. package/dist/components/foundation/modules/command/command.mjs +1 -1
  12. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  13. package/dist/components/foundation/modules/command/lifecycle.d.mts +81 -0
  14. package/dist/components/foundation/modules/command/lifecycle.mjs +2 -0
  15. package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -0
  16. package/dist/components/foundation/modules/observability/observability.d.mts +8 -0
  17. package/dist/components/foundation/modules/observability/observability.mjs +1 -1
  18. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  19. package/dist/components/foundation/operation.d.mts +6 -6
  20. package/dist/components/foundation/operation.mjs.map +1 -1
  21. package/dist/contracts.d.mts +4 -0
  22. package/dist/contracts.mjs +1 -0
  23. package/dist/crud.d.mts +2 -2
  24. package/dist/crud.mjs.map +1 -1
  25. package/dist/effect.d.mts +12 -0
  26. package/dist/effect.mjs +1 -0
  27. package/dist/errors.d.mts +2 -1
  28. package/dist/errors.mjs +1 -1
  29. package/dist/errors.mjs.map +1 -1
  30. package/dist/idempotency.d.mts +2 -0
  31. package/dist/idempotency.mjs +1 -0
  32. package/dist/modules/contracts/contract.d.mts +70 -0
  33. package/dist/modules/contracts/contract.mjs +2 -0
  34. package/dist/modules/contracts/contract.mjs.map +1 -0
  35. package/dist/modules/contracts/convex.d.mts +22 -0
  36. package/dist/modules/contracts/convex.mjs +2 -0
  37. package/dist/modules/contracts/convex.mjs.map +1 -0
  38. package/dist/modules/contracts/errors.d.mts +44 -0
  39. package/dist/modules/contracts/errors.mjs +2 -0
  40. package/dist/modules/contracts/errors.mjs.map +1 -0
  41. package/dist/modules/contracts/exposure.d.mts +105 -0
  42. package/dist/modules/contracts/exposure.mjs +2 -0
  43. package/dist/modules/contracts/exposure.mjs.map +1 -0
  44. package/dist/modules/contracts/idempotency.d.mts +87 -0
  45. package/dist/modules/contracts/idempotency.mjs +2 -0
  46. package/dist/modules/contracts/idempotency.mjs.map +1 -0
  47. package/dist/modules/contracts/standard.d.mts +16 -0
  48. package/dist/modules/contracts/standard.mjs +2 -0
  49. package/dist/modules/contracts/standard.mjs.map +1 -0
  50. package/dist/modules/contracts/workflow.d.mts +167 -0
  51. package/dist/modules/contracts/workflow.mjs +2 -0
  52. package/dist/modules/contracts/workflow.mjs.map +1 -0
  53. package/dist/modules/effect/agent-tools.d.mts +2 -0
  54. package/dist/modules/effect/agent-tools.mjs +1 -0
  55. package/dist/modules/effect/api-runtime.d.mts +28 -0
  56. package/dist/modules/effect/api-runtime.mjs +2 -0
  57. package/dist/modules/effect/api-runtime.mjs.map +1 -0
  58. package/dist/modules/effect/api-zod.d.mts +35 -0
  59. package/dist/modules/effect/api-zod.mjs +2 -0
  60. package/dist/modules/effect/api-zod.mjs.map +1 -0
  61. package/dist/modules/effect/api.d.mts +21 -0
  62. package/dist/modules/effect/api.mjs +2 -0
  63. package/dist/modules/effect/api.mjs.map +1 -0
  64. package/dist/modules/effect/client.d.mts +11 -0
  65. package/dist/modules/effect/client.mjs +1 -0
  66. package/dist/modules/effect/command.d.mts +59 -0
  67. package/dist/modules/effect/command.mjs +2 -0
  68. package/dist/modules/effect/command.mjs.map +1 -0
  69. package/dist/modules/effect/crud.d.mts +785 -0
  70. package/dist/modules/effect/crud.mjs +2 -0
  71. package/dist/modules/effect/crud.mjs.map +1 -0
  72. package/dist/modules/effect/errors.d.mts +8 -0
  73. package/dist/modules/effect/errors.mjs +2 -0
  74. package/dist/modules/effect/errors.mjs.map +1 -0
  75. package/dist/modules/effect/foundation.d.mts +21 -0
  76. package/dist/modules/effect/foundation.mjs +2 -0
  77. package/dist/modules/effect/foundation.mjs.map +1 -0
  78. package/dist/modules/effect/idempotency.d.mts +31 -0
  79. package/dist/modules/effect/idempotency.mjs +2 -0
  80. package/dist/modules/effect/idempotency.mjs.map +1 -0
  81. package/dist/modules/effect/normalize.d.mts +10 -0
  82. package/dist/modules/effect/normalize.mjs +2 -0
  83. package/dist/modules/effect/normalize.mjs.map +1 -0
  84. package/dist/modules/effect/operation.d.mts +141 -0
  85. package/dist/modules/effect/operation.mjs +2 -0
  86. package/dist/modules/effect/operation.mjs.map +1 -0
  87. package/dist/modules/effect/query.d.mts +65 -0
  88. package/dist/modules/effect/query.mjs +2 -0
  89. package/dist/modules/effect/query.mjs.map +1 -0
  90. package/dist/modules/effect/schema.d.mts +33 -0
  91. package/dist/modules/effect/schema.mjs +2 -0
  92. package/dist/modules/effect/schema.mjs.map +1 -0
  93. package/dist/modules/effect/workflow.d.mts +77 -0
  94. package/dist/modules/effect/workflow.mjs +2 -0
  95. package/dist/modules/effect/workflow.mjs.map +1 -0
  96. package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +79 -0
  97. package/dist/oxlint/architecture.d.mts +20 -0
  98. package/dist/oxlint/architecture.mjs +2 -0
  99. package/dist/oxlint/architecture.mjs.map +1 -0
  100. package/dist/oxlint/ast.d.mts +33 -0
  101. package/dist/oxlint/ast.mjs +2 -0
  102. package/dist/oxlint/ast.mjs.map +1 -0
  103. package/dist/oxlint/ownership.d.mts +62 -0
  104. package/dist/oxlint/ownership.mjs +2 -0
  105. package/dist/oxlint/ownership.mjs.map +1 -0
  106. package/dist/oxlint.d.mts +5 -2
  107. package/dist/oxlint.mjs +1 -1
  108. package/dist/oxlint.mjs.map +1 -1
  109. package/dist/workflow.d.mts +2 -0
  110. package/dist/workflow.mjs +1 -0
  111. package/docs/auth.md +65 -0
  112. package/docs/commands.md +118 -0
  113. package/docs/conventions.md +49 -21
  114. package/docs/domain-operations.md +615 -0
  115. package/docs/effect.md +384 -0
  116. package/docs/oxlint.md +74 -26
  117. package/docs/upgrading.md +13 -0
  118. package/package.json +37 -3
  119. package/scripts/convex-smoke-env.mjs +33 -0
  120. package/scripts/smoke-packed-components.mjs +9 -2
  121. package/scripts/smoke-packed-effect.mjs +439 -0
  122. package/scripts/test-packed-effect.mjs +457 -0
  123. package/scripts/test-packed-ids.mjs +1 -0
  124. package/scripts/test-packed-oxlint.mjs +24 -2
@@ -0,0 +1,457 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import {
3
+ cpSync,
4
+ mkdtempSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ realpathSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { basename, join, resolve } from 'node:path'
13
+
14
+ const root = resolve(import.meta.dirname, '..')
15
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
16
+ const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-effect-'))
17
+ const effectVersion = '4.0.1'
18
+ const helpersVersion = '0.1.123'
19
+ const nodeTypesVersion = '26.6.1'
20
+ const domainGuidePath = join(root, 'src/docs/domain-operations.md')
21
+ const domainGuideImports = new Set([
22
+ './fixture-effect/convex/_generated/api',
23
+ './fixture-effect/convex/_generated/server',
24
+ './fixture-effect/convex/idempotency',
25
+ './fixture-effect/convex/integratedRename',
26
+ './fixture-effect/convex/_generated/dataModel',
27
+ ])
28
+ const extraTypeFixtures = [
29
+ join(root, 'src/docs/effect.md'),
30
+ join(root, 'src/docs/commands.md'),
31
+ join(root, 'src/docs/auth.md'),
32
+ domainGuidePath,
33
+ ...process.argv.slice(2).map((path) => resolve(path)),
34
+ ]
35
+
36
+ function run(command, args, cwd) {
37
+ try {
38
+ return execFileSync(command, args, { cwd, encoding: 'utf8', stdio: 'pipe' })
39
+ } catch (error) {
40
+ // Install output can contain registry credentials. Report compiler/runtime diagnostics only.
41
+ if (!args.includes('install')) {
42
+ process.stderr.write(error.stdout ?? '')
43
+ process.stderr.write(error.stderr ?? '')
44
+ }
45
+ throw new Error(`${command} ${args.join(' ')} failed in ${basename(cwd)}`, {
46
+ cause: error.code,
47
+ })
48
+ }
49
+ }
50
+
51
+ function copyPublicTypeFixture(fixture, filename, publicImport) {
52
+ const source = readFileSync(join(root, 'test', filename), 'utf8')
53
+ .replaceAll(publicImport, 'cvx-kit/effect')
54
+ .replaceAll('../src/errors', 'cvx-kit/errors')
55
+ .replaceAll('../src/contracts', 'cvx-kit/contracts')
56
+ .replaceAll('../src/idempotency', 'cvx-kit/idempotency')
57
+ .replaceAll('../src/modules/contracts/errors', 'cvx-kit/errors')
58
+ .replaceAll('../src/modules/contracts/idempotency', 'cvx-kit/idempotency')
59
+ .replaceAll('../src/modules/contracts/workflow', 'cvx-kit/workflow')
60
+ .replaceAll('../src/modules/contracts/exposure', 'cvx-kit/agent-tools')
61
+ .replaceAll('../src/modules/effect/workflow', 'cvx-kit/effect')
62
+ .replaceAll('../src/modules/effect/idempotency', 'cvx-kit/effect')
63
+ .replaceAll('../src/modules/effect/foundation', 'cvx-kit/effect')
64
+ .replaceAll('../src/modules/effect/schema', 'cvx-kit/effect')
65
+ .replaceAll('../src/modules/effect/operation', 'cvx-kit/effect')
66
+ .replaceAll('../src/modules/effect/crud', 'cvx-kit/effect')
67
+ .replaceAll('../src/modules/effect/errors', 'cvx-kit/effect')
68
+ .replaceAll('../src/components/foundation/client', 'cvx-kit/components/foundation')
69
+ .replaceAll('../src/crud', 'cvx-kit/crud')
70
+ .replaceAll('../src/zod-table', 'cvx-kit/zod-table')
71
+ if (/from ['"]\.\.\/src\//.test(source))
72
+ throw new Error(`${filename} still uses private source imports`)
73
+ writeFileSync(join(fixture, filename), source)
74
+ return filename
75
+ }
76
+
77
+ // Compile the published guide itself so prose edits cannot leave stale companion fixtures passing.
78
+ function publishedTypeSources(path, contents) {
79
+ if (!path.endsWith('.md')) return [contents]
80
+ const marked = [
81
+ ...contents.matchAll(/<!-- packed-effect-example -->\s*```ts\s*\n([\s\S]*?)^```/gm),
82
+ ]
83
+ if (marked.length) return marked.map((match) => match[1])
84
+ return [[...contents.matchAll(/^```ts\s*\n([\s\S]*?)^```/gm)].map((match) => match[1]).join('\n')]
85
+ }
86
+
87
+ function copyExtraTypeFixtures(fixture, paths) {
88
+ return paths.flatMap((path, index) => {
89
+ const contents = readFileSync(path, 'utf8')
90
+ const sources = publishedTypeSources(path, contents)
91
+ const isDomainGuide = path === domainGuidePath
92
+ if (isDomainGuide) {
93
+ const companion = readFileSync(join(root, 'test/domain-operation-guide-types.ts'), 'utf8')
94
+ const blocks = [...contents.matchAll(/^```ts\s*\n([\s\S]*?)^```/gm)]
95
+ if (blocks.length !== 1 || sources.length !== 1 || sources[0].trim() !== companion.trim())
96
+ throw new Error(
97
+ 'Domain operation guide must contain its complete, current companion fixture',
98
+ )
99
+ cpSync(join(root, 'test/fixture-effect/convex'), join(fixture, 'fixture-effect/convex'), {
100
+ recursive: true,
101
+ })
102
+ }
103
+ return sources.map((source, snippet) => {
104
+ if (!source.trim())
105
+ throw new Error(`Documentation fixture ${basename(path)} has no TypeScript`)
106
+ const imports = [...source.matchAll(/from ['"]([^'"]+)['"]/g)].map((match) => match[1])
107
+ if (
108
+ imports.some(
109
+ (specifier) =>
110
+ /^cvx-kit\/(?:src|dist)\//.test(specifier) ||
111
+ (/^\.\.?\//.test(specifier) && !(isDomainGuide && domainGuideImports.has(specifier))),
112
+ )
113
+ ) {
114
+ throw new Error(`Documentation fixture ${basename(path)} must use public package imports`)
115
+ }
116
+ const filename = isDomainGuide
117
+ ? `domain-operation-guide-${snippet}.ts`
118
+ : `doc-example-${index}-${snippet}.ts`
119
+ writeFileSync(join(fixture, filename), source)
120
+ return filename
121
+ })
122
+ })
123
+ }
124
+
125
+ const legacyRuntime = `
126
+ import { strict as assert } from 'node:assert'
127
+ import { createRequire } from 'node:module'
128
+ import { Foundation } from 'cvx-kit/components/foundation'
129
+ import { Foundation as RootFoundation } from 'cvx-kit'
130
+ import { createAuthFunctions, defaultRoleMap } from 'cvx-kit/auth'
131
+ import { createCrudCommands } from 'cvx-kit/crud'
132
+ import { zodTable } from 'cvx-kit/zod-table'
133
+ import { decodeContract, standardContract, zodContract } from 'cvx-kit/contracts'
134
+ import { captureIdempotencyInvocation, transactionalIdempotency, canonicalConvexBytes } from 'cvx-kit/idempotency'
135
+ import { defineErrorContract } from 'cvx-kit/errors'
136
+ import { workflowTransition } from 'cvx-kit/workflow'
137
+ import { selectOperation, bindOperationExecutor, createOperationTools, operationToolDialect } from 'cvx-kit/agent-tools'
138
+ import { actionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalQueryGeneric, internalMutationGeneric } from 'convex/server'
139
+ import { z } from 'zod'
140
+ const require = createRequire(import.meta.url)
141
+ if (process.argv.includes('--without-effect')) {
142
+ assert.throws(() => require.resolve('effect'), { code: 'MODULE_NOT_FOUND' }, 'optional peer was unexpectedly auto-installed')
143
+ }
144
+ assert.equal(RootFoundation, Foundation)
145
+ assert.equal(typeof workflowTransition, 'function')
146
+ const toolOwner = Symbol('packed-tools')
147
+ const toolSelection = selectOperation(toolOwner, 'query', 'measure', { input: z.object({ title: z.string() }), result: z.number() })
148
+ const toolExecutor = bindOperationExecutor(toolSelection, { owner: toolOwner, key: 'measure', execute: async raw => raw.title.length })
149
+ const operationTools = createOperationTools({ measure: { operation: toolSelection, executor: toolExecutor, description: 'Measure a title', converter: { dialect: operationToolDialect, convert: () => ({ type: 'object', properties: { title: { type: 'string' } }, required: ['title'], additionalProperties: false }) } } })
150
+ assert.deepEqual(await operationTools.measure.invoke({ title: 'packed' }), { _tag: 'Success', value: 6 })
151
+ let normalizations = 0
152
+ const normalized = zodContract(z.string().transform(async value => { normalizations++; return value.trim() }))
153
+ assert.deepEqual(await decodeContract(normalized, ' packed '), { value: 'packed' })
154
+ assert.equal(normalizations, 1)
155
+ const receiverSchema = { '~standard': {
156
+ version: 1, vendor: 'packed-receiver',
157
+ validate(value) { return this.vendor === 'packed-receiver' ? { value } : { issues: [{ message: 'lost receiver' }] } },
158
+ } }
159
+ assert.deepEqual(await decodeContract(receiverSchema, 'packed'), { value: 'packed' })
160
+ assert.deepEqual(await decodeContract(standardContract(receiverSchema), 'packed'), { value: 'packed' })
161
+ const errors = defineErrorContract({ DENIED: { message: 'Denied', details: { key: z.string() } } })
162
+ const envelope = errors.project(errors.create('DENIED', { key: 'packed' }))
163
+ assert.deepEqual(errors.decode(JSON.parse(JSON.stringify(envelope))), envelope)
164
+ const audits = []
165
+ const { Command } = new Foundation({ functions: { status: null } }, {
166
+ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }), writeAudit: (_ctx, entry) => audits.push(entry) },
167
+ })
168
+ const operations = { measure: Command.operation({
169
+ command: z.object({ title: z.string().transform(value => value.length) }), result: z.number(),
170
+ classification: 'business', audit: () => null,
171
+ }) }
172
+ const execute = new Command(operations).exec({ operation: 'measure', handler: async (ctx, input) => ctx.offset + input.title })
173
+ const pending = execute({ offset: 2 }, { title: 'abc' })
174
+ assert.ok(pending instanceof Promise)
175
+ assert.equal(await pending, 5)
176
+ assert.equal(defaultRoleMap('member'), 'writer')
177
+ const auth = createAuthFunctions({
178
+ query: queryGeneric, mutation: mutationGeneric, action: actionGeneric,
179
+ internalQuery: internalQueryGeneric, internalMutation: internalMutationGeneric, internalAction: internalActionGeneric,
180
+ getAuthUser: async () => ({ id: 'actor' }), mapRole: defaultRoleMap, adminRoles: ['admin'],
181
+ })
182
+ const authenticated = auth.authQuery({ args: {}, returns: z.string(), handler: ctx => ctx.actor.userId + ':' + ctx.tenant })
183
+ assert.equal(await authenticated._handler({ auth: { getUserIdentity: async () => ({ subject: 'actor', org_id: 'org', role: 'member' }) }, db: {} }, {}), 'actor:org')
184
+ const table = zodTable('notes', () => ({ text: z.string() }), { commandFields: ['text'] })
185
+ const crud = createCrudCommands({ Command, table, aggregateType: 'note', actor: () => 'actor' })
186
+ const changes = []
187
+ const ctx = { db: { insert: async (name, row) => { changes.push([name, row]); return 'note-id' }, patch: async (id, row) => changes.push([id, row]) } }
188
+ assert.deepEqual(await crud.executeCreate(ctx, { text: 'packed' }), { id: 'note-id' })
189
+ assert.deepEqual(await crud.executeUpdate(ctx, { id: 'note-id', data: { text: 'updated' } }), { ok: true })
190
+ assert.deepEqual(changes, [['notes', { text: 'packed' }], ['note-id', { text: 'updated' }]])
191
+ assert.equal(audits.length, 2)
192
+ const bounds = { maxDepth: 8, maxNodes: 100, maxBytes: 4096, maxArrayLength: 20, maxObjectFields: 20 }
193
+ assert.equal(canonicalConvexBytes({ b: { d: 2, c: 1 }, a: 0 }, bounds), canonicalConvexBytes({ a: 0, b: { c: 1, d: 2 } }, bounds))
194
+ const invocation = captureIdempotencyInvocation({ binding: { kind: 'mutation', atomicity: 'same-mutation' }, identity: { operation: 'packed', scope: 'host', key: 'one' }, versions: { operation: '1', contract: '1', binding: '1', fingerprintPolicy: '1' }, rawInput: { title: 'raw' }, canonical: { bounds } })
195
+ const ledger = []
196
+ let authorizations = 0
197
+ const idempotency = transactionalIdempotency({
198
+ final: { replayResult: zodContract(z.object({ version: z.literal(1), value: z.string() }).transform(wire => wire.value)), encode: value => ({ version: 1, value }) },
199
+ authorize: () => { authorizations++ }, lookup: () => ledger, fingerprint: bytes => bytes,
200
+ claim: (_identity, receipt) => { ledger.push(receipt); return 0 }, complete: (index, receipt) => { ledger[index] = receipt },
201
+ })
202
+ const preparation = await idempotency.prepare(invocation)
203
+ assert.equal(preparation.kind, 'execute')
204
+ await preparation.complete('final!')
205
+ const replay = await idempotency.prepare(invocation)
206
+ assert.equal(replay.kind, 'replay')
207
+ assert.deepEqual(await decodeContract(idempotency.replayResult, replay.result), { value: 'final!' })
208
+ assert.equal(authorizations, 2)
209
+ assert.equal(ledger.length, 1)
210
+ `
211
+
212
+ const legacyTypes = `
213
+ import { Foundation } from 'cvx-kit'
214
+ import { createAuthFunctions, defaultRoleMap } from 'cvx-kit/auth'
215
+ import { createCrudCommands } from 'cvx-kit/crud'
216
+ import { zodTable } from 'cvx-kit/zod-table'
217
+ import { actionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalQueryGeneric, internalMutationGeneric, type GenericMutationCtx, type GenericDataModel } from 'convex/server'
218
+ import { z } from 'zod'
219
+ const { Command } = new Foundation({ functions: { status: null } }, {
220
+ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) },
221
+ })
222
+ const operation = Command.withContext<{ offset: number }>().operation({
223
+ command: z.object({ title: z.string().transform(value => value.length) }), result: z.number(), classification: 'business', audit: () => null,
224
+ })
225
+ const commands = new Command<{ offset: number }, { measure: typeof operation }>({ measure: operation })
226
+ const execute = commands.exec({ operation: 'measure', handler: async (ctx, input) => ctx.offset + input.title })
227
+ const result: Promise<number> = execute({ offset: 1 }, { title: 'raw' })
228
+ void result
229
+ const auth = createAuthFunctions({ query: queryGeneric, mutation: mutationGeneric, action: actionGeneric,
230
+ internalQuery: internalQueryGeneric, internalMutation: internalMutationGeneric, internalAction: internalActionGeneric,
231
+ getAuthUser: async () => ({ id: 'actor' }), mapRole: defaultRoleMap, adminRoles: ['admin'],
232
+ })
233
+ const authenticated = auth.authQuery({ args: {}, returns: z.string(), handler: ctx => ctx.actor.userId })
234
+ void authenticated
235
+ const table = zodTable('notes', () => ({ text: z.string() }), { commandFields: ['text'] })
236
+ const crud = createCrudCommands<GenericMutationCtx<GenericDataModel>, typeof table>({ Command, table, aggregateType: 'note', actor: () => 'actor' })
237
+ declare const ctx: GenericMutationCtx<GenericDataModel>
238
+ void crud.executeCreate(ctx, { text: 'packed' })
239
+ `
240
+
241
+ const domainTypes = `
242
+ import { Context, Effect } from 'effect'
243
+ import { createEffectFoundation } from 'cvx-kit/effect'
244
+ import { z } from 'zod'
245
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
246
+ type Assert<T extends true> = T
247
+ class Reader extends Context.Service<Reader, { count: number }>()('PackedReader') {}
248
+ class Failure { readonly _tag = 'PackedFailure' }
249
+ const foundation = createEffectFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => undefined })
250
+ const queries = foundation.Query({ context: (host: { actor: string }) => host, operations: ({ query }) => ({
251
+ read: query({ input: z.string().transform(value => value.length), result: z.number().transform(String),
252
+ handler: (input, ctx) => Effect.gen(function* () { ctx.actor.toUpperCase(); if (input < 1) return yield* Effect.fail(new Failure()); return input + (yield* Reader).count }),
253
+ }),
254
+ recovered: query({ input: z.number(), result: z.number(), handler: input => Effect.gen(function* () { if (input < 0) return yield* Effect.fail(new Failure()); return input + (yield* Reader).count }),
255
+ middleware: ({ next }) => next().pipe(Effect.provideService(Reader, { count: 1 }), Effect.catchTag('PackedFailure', () => Effect.succeed(0))),
256
+ }),
257
+ ordinary: query({ input: z.number(), result: z.number(), handler: input => input }),
258
+ promise: query({ input: z.number(), result: z.number(), handler: async input => input }),
259
+ named: query({ input: z.number(), result: z.number(), handler: Effect.fn('packed.named')(function* (input: number) { return yield* Effect.succeed(input) }) }),
260
+ }) })
261
+ const read = queries.exec('read', 'raw', { actor: 'trusted' })
262
+ const recovered = queries.exec('recovered', 1, { actor: 'trusted' })
263
+ const checks: [Assert<Equal<Effect.Success<typeof read>, string>>, Assert<Equal<Effect.Error<typeof read>, Failure>>, Assert<Equal<Effect.Services<typeof read>, Reader>>, Assert<Equal<Effect.Error<typeof recovered>, never>>, Assert<Equal<Effect.Services<typeof recovered>, never>>] = [true, true, true, true, true]
264
+ void checks
265
+ // @ts-expect-error an unprovided domain requirement cannot reach the runner
266
+ void Effect.runPromise(read)
267
+ // @ts-expect-error transport input is the raw schema input
268
+ queries.exec('read', 1, { actor: 'trusted' })
269
+ for (const kind of ['ordinary', 'promise', 'named'] as const) { const execution = queries.exec(kind, 1, { actor: 'trusted' }); const checks: [Assert<Equal<Effect.Error<typeof execution>, never>>, Assert<Equal<Effect.Services<typeof execution>, never>>] = [true, true]; void checks }
270
+ `
271
+
272
+ const effectRuntime = `
273
+ import { strict as assert } from 'node:assert'
274
+ import { createRequire } from 'node:module'
275
+ import { realpathSync } from 'node:fs'
276
+ import { Context, Effect } from 'effect'
277
+ import { createEffectFoundation, createEffectCrud, effectApiBuilder, effectZodApiBuilder, createOperationTools } from 'cvx-kit/effect'
278
+ import { createOperationTools as neutralOperationTools } from 'cvx-kit/agent-tools'
279
+ import { zodTable } from 'cvx-kit/zod-table'
280
+ import { queryGeneric, internalMutationGeneric, actionGeneric } from 'convex/server'
281
+ import { zCustomMutation } from 'convex-helpers/server/zod4'
282
+ import { v } from 'convex/values'
283
+ import { z } from 'zod'
284
+ const require = createRequire(import.meta.url)
285
+ const packageRequire = createRequire(import.meta.resolve('cvx-kit/effect'))
286
+ assert.equal(createOperationTools, neutralOperationTools)
287
+ assert.equal(realpathSync(require.resolve('effect')), realpathSync(packageRequire.resolve('effect')), 'package resolved a second Effect identity')
288
+ class Request extends Context.Service()('PackedRequest') {}
289
+ const events = []
290
+ const foundation = createEffectFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => events.push('audit') })
291
+ const queries = foundation.Query({ context: host => host, operations: ({ query }) => ({
292
+ read: query({ input: z.string().transform(value => value.length), result: z.number(), handler: input => Effect.gen(function* () { events.push('read'); return input + (yield* Request).offset }) }),
293
+ }) })
294
+ const commands = foundation.Command({ context: host => host, operations: ({ command }) => ({
295
+ save: command({ input: z.string(), result: z.number(), classification: 'business', audit: () => ({ operation: 'save', actorId: 'actor', aggregate: { type: 'note', id: 'note-id' } }), handler: (input, host) => Effect.gen(function* () { events.push('save'); return yield* queries.exec('read', input, host) }) }),
296
+ }) })
297
+ const execution = commands.exec('save', 'abc', {})
298
+ assert.ok(Effect.isEffect(execution), 'public facade must return the host Effect value')
299
+ assert.deepEqual(events, [], 'domain composition must stay lazy')
300
+ const adapted = effectApiBuilder(queryGeneric, { services: () => { events.push('provide'); return Context.make(Request, { offset: 2 }) } })({ args: {}, returns: v.number(), handler: () => execution })
301
+ assert.equal(await adapted._handler({}, {}), 5)
302
+ assert.deepEqual(events, ['provide', 'save', 'read', 'audit'], 'one adapter run must execute nested composition once')
303
+ const plain = effectApiBuilder(queryGeneric, { services: () => Context.empty() })
304
+ for (const handler of [() => 3, () => Promise.resolve(3), () => Effect.succeed(3), Effect.fn('packed.fn')(function* () { return yield* Effect.succeed(3) })]) {
305
+ assert.equal(await plain({ handler })._handler({}, {}), 3)
306
+ }
307
+ const customization = []
308
+ const custom = effectZodApiBuilder(zCustomMutation(internalMutationGeneric, { args: { session: v.string() }, input: (_ctx, args) => { customization.push('custom'); return { ctx: { actor: args.session }, args: { made: 4 } } } }), {
309
+ services: ctx => { customization.push('provide:' + ctx.actor); return Context.make(Request, { offset: 2 }) },
310
+ })({ args: { amount: z.string().transform(value => value.length) }, returns: z.number().transform(String), skipConvexValidation: true,
311
+ handler: (ctx, args) => Effect.gen(function* () { customization.push('handle:' + ctx.actor); return args.amount + args.made + (yield* Request).offset }),
312
+ })
313
+ assert.equal(await custom._handler({}, { session: 'trusted', amount: 'abc' }), '9')
314
+ assert.deepEqual(customization, ['custom', 'provide:trusted', 'handle:trusted'])
315
+ assert.ok(custom.isInternal && custom.isMutation)
316
+ assert.ok(effectApiBuilder(actionGeneric, { services: () => Context.empty() })(() => 1).isAction)
317
+ for (const fail of [false, true]) {
318
+ const lifecycle = []
319
+ const adapter = effectApiBuilder(queryGeneric, { services: () => Effect.acquireRelease(Effect.sync(() => { lifecycle.push('acquire'); return Context.empty() }), () => Effect.promise(async () => { await Promise.resolve(); lifecycle.push('release') })) })
320
+ const registered = adapter({ handler: () => Effect.gen(function* () { lifecycle.push('handle'); if (fail) return yield* Effect.fail('packed-failure'); return 1 }) })
321
+ if (fail) await assert.rejects(registered._handler({}, {}), error => error === 'packed-failure')
322
+ else assert.equal(await registered._handler({}, {}), 1)
323
+ assert.deepEqual(lifecycle, ['acquire', 'handle', 'release'])
324
+ }
325
+ await assert.rejects(plain({ handler: () => Effect.die(new Error('packed-defect')) })._handler({}, {}), /packed-defect/)
326
+ await assert.rejects(plain({ handler: () => Effect.interrupt })._handler({}, {}))
327
+ const crudAudits = []
328
+ const crudFoundation = createEffectFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: (_host, entry) => crudAudits.push(entry) })
329
+ const crudTable = zodTable('packedNotes', () => ({ title: z.string(), secret: z.string() }), { commandFields: ['title'], serverFields: ['secret'], publicFields: ['title'] })
330
+ let crudRow
331
+ const crudHost = { actor: 'packed-actor', db: { insert: async (_table, values) => { crudRow = values; return 'packed-id' }, patch: async (_id, values) => { crudRow = { ...crudRow, ...values } } } }
332
+ const crud = createEffectCrud({ foundation: crudFoundation, table: crudTable, context: host => host, aggregateType: 'note', actor: host => host.actor, checkId: (_host, id) => id === 'packed-id', enrich: () => ({ secret: 'private' }), read: { maxPageSize: 2, project: row => ({ title: row.title }), get: () => crudRow ?? null, list: () => ({ page: crudRow ? [crudRow] : [], isDone: true, continueCursor: '' }) } })
333
+ const pendingCreate = crud.commands.exec('packedNotes.create', { title: 'packed-crud' }, crudHost)
334
+ assert.equal(crudRow, undefined, 'CRUD remains lazy before the API runner')
335
+ assert.deepEqual(await Effect.runPromise(pendingCreate), { id: 'packed-id' })
336
+ assert.deepEqual(await Effect.runPromise(crud.queries.exec('packedNotes.get', { id: 'packed-id' }, crudHost)), { title: 'packed-crud' })
337
+ assert.deepEqual(await Effect.runPromise(crud.commands.exec('packedNotes.update', { id: 'packed-id', data: {} }, crudHost)), { ok: true })
338
+ assert.equal(crudAudits.length, 2)
339
+ assert.deepEqual(crudRow, { title: 'packed-crud', secret: 'private' })
340
+ assert.deepEqual(await Effect.runPromise(crud.commands.exec('packedNotes.update', { id: 'packed-id', data: { title: 'packed-updated' } }, crudHost)), { ok: true })
341
+ assert.deepEqual(crudRow, { title: 'packed-updated', secret: 'private' })
342
+ assert.deepEqual(await Effect.runPromise(crud.queries.exec('packedNotes.get', { id: 'packed-id' }, crudHost)), { title: 'packed-updated' })
343
+ assert.equal(crudAudits.length, 3)
344
+ assert.deepEqual(crudAudits.map(entry => entry.operation), ['packedNotes.create', 'packedNotes.update', 'packedNotes.update'])
345
+ for (const [registry, operation, input] of [
346
+ [crud.queries, 'packedNotes.get', { id: 'other-table-id' }],
347
+ [crud.commands, 'packedNotes.update', { id: 'other-table-id', data: { title: 'wrong-table' } }],
348
+ [crud.commands, 'packedNotes.archive', { id: 'other-table-id' }],
349
+ ]) {
350
+ await assert.rejects(Effect.runPromise(registry.exec(operation, input, crudHost)), error => error.code === 'CRUD_INVALID_ID')
351
+ assert.deepEqual(crudRow, { title: 'packed-updated', secret: 'private' })
352
+ assert.equal(crudAudits.length, 3)
353
+ }
354
+ `
355
+
356
+ try {
357
+ run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
358
+ for (const installer of ['npm', 'bun']) {
359
+ for (const withEffect of [false, true]) {
360
+ const name = `${installer}-${withEffect ? 'effect' : 'legacy'}`
361
+ const fixture = join(temporary, name)
362
+ mkdirSync(fixture)
363
+ const dependencies = {
364
+ 'cvx-kit': `file:${join(temporary, `cvx-kit-${manifest.version}.tgz`)}`,
365
+ convex: manifest.devDependencies.convex,
366
+ 'convex-helpers': helpersVersion,
367
+ '@convex-dev/workflow': manifest.dependencies['@convex-dev/workflow'],
368
+ zod: manifest.devDependencies.zod,
369
+ }
370
+ if (withEffect) dependencies.effect = effectVersion
371
+ const devDependencies = { typescript: manifest.devDependencies.typescript }
372
+ if (withEffect) devDependencies['@types/node'] = nodeTypesVersion
373
+ writeFileSync(
374
+ join(fixture, 'package.json'),
375
+ JSON.stringify({
376
+ private: true,
377
+ type: 'module',
378
+ dependencies,
379
+ devDependencies,
380
+ }),
381
+ )
382
+ run(installer, ['install', '--ignore-scripts'], fixture)
383
+ writeFileSync(join(fixture, 'legacy.mjs'), legacyRuntime)
384
+ writeFileSync(join(fixture, 'legacy-types.ts'), legacyTypes)
385
+ const typeFiles = [
386
+ 'legacy-types.ts',
387
+ copyPublicTypeFixture(fixture, 'operation-neutral-types.ts', '../src/effect'),
388
+ copyPublicTypeFixture(fixture, 'operation-agent-tools-types.ts', '../src/effect'),
389
+ ]
390
+ const guideTypeFiles = []
391
+ if (withEffect) {
392
+ for (const filename of [
393
+ 'effect-foundation-types.ts',
394
+ 'effect-api-types.ts',
395
+ 'effect-api-zod-types.ts',
396
+ 'effect-registry-review-types.ts',
397
+ 'operation-contract-types.ts',
398
+ 'effect-schema-types.ts',
399
+ 'effect-crud-types.ts',
400
+ 'idempotency-types.ts',
401
+ 'operation-workflow-types.ts',
402
+ 'error-contract-types.ts',
403
+ ]) {
404
+ typeFiles.push(copyPublicTypeFixture(fixture, filename, '../src/effect'))
405
+ }
406
+ writeFileSync(join(fixture, 'domain-types.ts'), domainTypes)
407
+ writeFileSync(join(fixture, 'effect.mjs'), effectRuntime)
408
+ typeFiles.push('domain-types.ts')
409
+ for (const filename of copyExtraTypeFixtures(fixture, extraTypeFixtures)) {
410
+ if (filename.startsWith('domain-operation-guide-')) guideTypeFiles.push(filename)
411
+ else typeFiles.push(filename)
412
+ }
413
+ }
414
+ writeFileSync(
415
+ join(fixture, 'tsconfig.json'),
416
+ JSON.stringify({
417
+ compilerOptions: {
418
+ noEmit: true,
419
+ strict: true,
420
+ skipLibCheck: false,
421
+ target: 'ES2025',
422
+ lib: ['ES2025', 'DOM', 'ESNext.Disposable'],
423
+ module: 'ESNext',
424
+ moduleResolution: 'Bundler',
425
+ types: [],
426
+ },
427
+ files: typeFiles,
428
+ }),
429
+ )
430
+ const compiler = realpathSync(join(fixture, 'node_modules/typescript/bin/tsc'))
431
+ run('node', [compiler, '-p', 'tsconfig.json'], fixture)
432
+ if (withEffect) {
433
+ writeFileSync(
434
+ join(fixture, 'tsconfig.domain.json'),
435
+ JSON.stringify({
436
+ extends: './tsconfig.json',
437
+ compilerOptions: { types: ['node'] },
438
+ files: guideTypeFiles,
439
+ }),
440
+ )
441
+ run('node', [compiler, '-p', 'tsconfig.domain.json'], fixture)
442
+ console.log(`${name}: complete domain guide and actual native references pass separately`)
443
+ }
444
+ console.log(
445
+ `${name}: strict public declaration/type fixtures pass (TypeScript ${manifest.devDependencies.typescript}, helpers ${helpersVersion})`,
446
+ )
447
+ const runtime = installer === 'bun' ? 'bun' : 'node'
448
+ run(runtime, ['legacy.mjs', ...(withEffect ? [] : ['--without-effect'])], fixture)
449
+ if (withEffect) run(runtime, ['effect.mjs'], fixture)
450
+ console.log(
451
+ `${name}: public consumer runtime pass (Convex ${manifest.devDependencies.convex}, Zod ${manifest.devDependencies.zod}${withEffect ? `, Effect ${effectVersion}` : ', Effect absent'})`,
452
+ )
453
+ }
454
+ }
455
+ } finally {
456
+ rmSync(temporary, { recursive: true, force: true })
457
+ }
@@ -66,6 +66,7 @@ for (const validator of [
66
66
  for (const testFile of ['command.test.ts', 'command-lifecycle.test.ts']) {
67
67
  const source = readFileSync(join(root, 'test', testFile), 'utf8')
68
68
  .replace('../src/components/foundation/client', 'cvx-kit/components/foundation')
69
+ .replaceAll('../src/contracts', 'cvx-kit/contracts')
69
70
  .replace(
70
71
  "const modules = import.meta.glob('./fixture/**/*.ts')",
71
72
  "const modules = { './_generated/server.ts': async () => ({}) }",
@@ -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, 15)
12
+ assert.equal(ruleNames.length, 20)
13
13
 
14
14
  try {
15
15
  run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
@@ -48,6 +48,13 @@ try {
48
48
  join(fixture, 'convex/components/example/client.ts'),
49
49
  'export function run() { return 1 }\n',
50
50
  )
51
+ for (const directory of ['api', 'components/example']) {
52
+ mkdirSync(join(fixture, 'convex', directory, '__tests__'), { recursive: true })
53
+ writeFileSync(
54
+ join(fixture, 'convex', directory, '__tests__/behavior.test.ts'),
55
+ 'test("behavior", () => { expect(1).toBe(1) })',
56
+ )
57
+ }
51
58
  const oxlint = join(fixture, 'node_modules/.bin/oxlint')
52
59
  run(oxlint, ['--config', '.oxlintrc.json', 'convex'], fixture)
53
60
  writeFileSync(
@@ -83,6 +90,19 @@ export const target = ref('orders:save')
83
90
  export const value = other
84
91
  `,
85
92
  )
93
+ mkdirSync(join(fixture, 'convex/application'), { recursive: true })
94
+ writeFileSync(
95
+ join(fixture, 'convex/application/provider.ts'),
96
+ "import Stripe from 'stripe'; export const provider = new Stripe('key')",
97
+ )
98
+ writeFileSync(
99
+ join(fixture, 'convex/domain/orders/queries.ts'),
100
+ "export function save(ctx) { return ctx.db.insert('orders', {}) }",
101
+ )
102
+ writeFileSync(
103
+ join(fixture, 'convex/api/internal.ts'),
104
+ "import { systemMutation } from '../functions'; export const internal = systemMutation({})",
105
+ )
86
106
  const invalid = spawnSync(
87
107
  oxlint,
88
108
  ['--config', '.oxlintrc.json', '--format', 'json', 'convex'],
@@ -103,11 +123,13 @@ export const value = other
103
123
  writeFileSync(
104
124
  join(fixture, 'plugin-types.ts'),
105
125
  `
106
- import plugin, { rules, type RuleName } from 'cvx-kit/oxlint'
126
+ import plugin, { rules, checkArchitecture, type ArchitectureDiagnostic, type RuleName } from 'cvx-kit/oxlint'
107
127
  import type { Plugin, Rule } from '@oxlint/plugins'
108
128
  const compatible: Plugin = plugin
109
129
  const name: RuleName = 'component-boundaries'
110
130
  const rule: Rule = rules[name]
131
+ const issues: ArchitectureDiagnostic[] = checkArchitecture()
132
+ void issues
111
133
  void compatible
112
134
  void rule
113
135
  `,