@opensaas/stack-core 0.37.0 → 0.39.0

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 (226) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +191 -0
  3. package/CLAUDE.md +42 -3
  4. package/dist/access/access-filter.d.ts +70 -113
  5. package/dist/access/access-filter.d.ts.map +1 -1
  6. package/dist/access/access-filter.js +183 -210
  7. package/dist/access/access-filter.js.map +1 -1
  8. package/dist/access/access-filter.test.js +148 -188
  9. package/dist/access/access-filter.test.js.map +1 -1
  10. package/dist/access/declared-dependencies.d.ts +66 -26
  11. package/dist/access/declared-dependencies.d.ts.map +1 -1
  12. package/dist/access/declared-dependencies.js +62 -17
  13. package/dist/access/declared-dependencies.js.map +1 -1
  14. package/dist/access/declared-dependencies.test.d.ts +2 -0
  15. package/dist/access/declared-dependencies.test.d.ts.map +1 -0
  16. package/dist/access/declared-dependencies.test.js +226 -0
  17. package/dist/access/declared-dependencies.test.js.map +1 -0
  18. package/dist/access/depth-limits.d.ts +8 -7
  19. package/dist/access/depth-limits.d.ts.map +1 -1
  20. package/dist/access/depth-limits.js +8 -7
  21. package/dist/access/depth-limits.js.map +1 -1
  22. package/dist/access/engine.d.ts +0 -20
  23. package/dist/access/engine.d.ts.map +1 -1
  24. package/dist/access/engine.js +0 -25
  25. package/dist/access/engine.js.map +1 -1
  26. package/dist/access/errors.d.ts +61 -8
  27. package/dist/access/errors.d.ts.map +1 -1
  28. package/dist/access/errors.js +91 -12
  29. package/dist/access/errors.js.map +1 -1
  30. package/dist/access/field-access.d.ts +39 -1
  31. package/dist/access/field-access.d.ts.map +1 -1
  32. package/dist/access/field-access.js +108 -42
  33. package/dist/access/field-access.js.map +1 -1
  34. package/dist/access/field-access.test.js +229 -2
  35. package/dist/access/field-access.test.js.map +1 -1
  36. package/dist/access/field-transforms.d.ts +0 -27
  37. package/dist/access/field-transforms.d.ts.map +1 -1
  38. package/dist/access/field-visibility.d.ts +2 -5
  39. package/dist/access/field-visibility.d.ts.map +1 -1
  40. package/dist/access/field-visibility.js +89 -41
  41. package/dist/access/field-visibility.js.map +1 -1
  42. package/dist/access/index.d.ts +6 -3
  43. package/dist/access/index.d.ts.map +1 -1
  44. package/dist/access/index.js +12 -2
  45. package/dist/access/index.js.map +1 -1
  46. package/dist/access/query-validation.d.ts +121 -0
  47. package/dist/access/query-validation.d.ts.map +1 -0
  48. package/dist/access/query-validation.js +292 -0
  49. package/dist/access/query-validation.js.map +1 -0
  50. package/dist/access/relationship-count.d.ts +1 -1
  51. package/dist/access/relationship-count.d.ts.map +1 -1
  52. package/dist/access/relationship-count.js +2 -10
  53. package/dist/access/relationship-count.js.map +1 -1
  54. package/dist/access/relationship-label-filter.d.ts +23 -23
  55. package/dist/access/relationship-label-filter.d.ts.map +1 -1
  56. package/dist/access/relationship-label-filter.js +5 -76
  57. package/dist/access/relationship-label-filter.js.map +1 -1
  58. package/dist/access/relationship-label-filter.test.js +19 -56
  59. package/dist/access/relationship-label-filter.test.js.map +1 -1
  60. package/dist/access/transaction-registry.d.ts +45 -0
  61. package/dist/access/transaction-registry.d.ts.map +1 -0
  62. package/dist/access/transaction-registry.js +37 -0
  63. package/dist/access/transaction-registry.js.map +1 -0
  64. package/dist/access/types.d.ts +55 -59
  65. package/dist/access/types.d.ts.map +1 -1
  66. package/dist/config/index.d.ts +9 -7
  67. package/dist/config/index.d.ts.map +1 -1
  68. package/dist/config/index.js +10 -18
  69. package/dist/config/index.js.map +1 -1
  70. package/dist/config/label.d.ts +6 -13
  71. package/dist/config/label.d.ts.map +1 -1
  72. package/dist/config/label.js +6 -13
  73. package/dist/config/label.js.map +1 -1
  74. package/dist/config/nav-count.d.ts +11 -19
  75. package/dist/config/nav-count.d.ts.map +1 -1
  76. package/dist/config/nav-count.js +11 -19
  77. package/dist/config/nav-count.js.map +1 -1
  78. package/dist/config/plugin-engine.d.ts +0 -10
  79. package/dist/config/plugin-engine.d.ts.map +1 -1
  80. package/dist/config/plugin-engine.js +1 -37
  81. package/dist/config/plugin-engine.js.map +1 -1
  82. package/dist/config/types.d.ts +94 -109
  83. package/dist/config/types.d.ts.map +1 -1
  84. package/dist/context/apply-defaults.d.ts +8 -31
  85. package/dist/context/apply-defaults.d.ts.map +1 -1
  86. package/dist/context/apply-defaults.js +12 -41
  87. package/dist/context/apply-defaults.js.map +1 -1
  88. package/dist/context/hook-pipeline.d.ts +4 -29
  89. package/dist/context/hook-pipeline.d.ts.map +1 -1
  90. package/dist/context/hook-pipeline.js +8 -41
  91. package/dist/context/hook-pipeline.js.map +1 -1
  92. package/dist/context/index.d.ts +4 -15
  93. package/dist/context/index.d.ts.map +1 -1
  94. package/dist/context/index.js +223 -215
  95. package/dist/context/index.js.map +1 -1
  96. package/dist/context/nested-operations.d.ts +7 -26
  97. package/dist/context/nested-operations.d.ts.map +1 -1
  98. package/dist/context/nested-operations.js +73 -171
  99. package/dist/context/nested-operations.js.map +1 -1
  100. package/dist/context/transaction-boundary.d.ts +36 -47
  101. package/dist/context/transaction-boundary.d.ts.map +1 -1
  102. package/dist/context/transaction-boundary.js +92 -79
  103. package/dist/context/transaction-boundary.js.map +1 -1
  104. package/dist/context/write-pipeline.d.ts +25 -64
  105. package/dist/context/write-pipeline.d.ts.map +1 -1
  106. package/dist/context/write-pipeline.js +81 -111
  107. package/dist/context/write-pipeline.js.map +1 -1
  108. package/dist/fields/format-prisma-default.d.ts.map +1 -1
  109. package/dist/fields/format-prisma-default.js +4 -7
  110. package/dist/fields/format-prisma-default.js.map +1 -1
  111. package/dist/fields/index.d.ts +17 -2
  112. package/dist/fields/index.d.ts.map +1 -1
  113. package/dist/fields/index.js +136 -147
  114. package/dist/fields/index.js.map +1 -1
  115. package/dist/filter/collect.d.ts +26 -3
  116. package/dist/filter/collect.d.ts.map +1 -1
  117. package/dist/filter/collect.js +30 -12
  118. package/dist/filter/collect.js.map +1 -1
  119. package/dist/filter/filter.test.js +98 -15
  120. package/dist/filter/filter.test.js.map +1 -1
  121. package/dist/filter/index.d.ts +1 -0
  122. package/dist/filter/index.d.ts.map +1 -1
  123. package/dist/filter/index.js.map +1 -1
  124. package/dist/filter/map.d.ts.map +1 -1
  125. package/dist/filter/map.js +0 -5
  126. package/dist/filter/map.js.map +1 -1
  127. package/dist/filter/parse.d.ts.map +1 -1
  128. package/dist/filter/parse.js +2 -9
  129. package/dist/filter/parse.js.map +1 -1
  130. package/dist/filter/serialize.d.ts.map +1 -1
  131. package/dist/filter/serialize.js +2 -4
  132. package/dist/filter/serialize.js.map +1 -1
  133. package/dist/hooks/index.d.ts +4 -42
  134. package/dist/hooks/index.d.ts.map +1 -1
  135. package/dist/hooks/index.js +5 -59
  136. package/dist/hooks/index.js.map +1 -1
  137. package/dist/index.d.ts +3 -1
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +20 -4
  140. package/dist/index.js.map +1 -1
  141. package/dist/internal.d.ts +1 -0
  142. package/dist/internal.d.ts.map +1 -1
  143. package/dist/internal.js +4 -0
  144. package/dist/internal.js.map +1 -1
  145. package/dist/lib/case-utils.d.ts +0 -32
  146. package/dist/lib/case-utils.d.ts.map +1 -1
  147. package/dist/lib/case-utils.js +0 -32
  148. package/dist/lib/case-utils.js.map +1 -1
  149. package/dist/mcp/handler.d.ts +0 -4
  150. package/dist/mcp/handler.d.ts.map +1 -1
  151. package/dist/mcp/handler.js +12 -69
  152. package/dist/mcp/handler.js.map +1 -1
  153. package/dist/query/index.d.ts +55 -227
  154. package/dist/query/index.d.ts.map +1 -1
  155. package/dist/query/index.js +41 -117
  156. package/dist/query/index.js.map +1 -1
  157. package/dist/query/relationship-options.d.ts +1 -1
  158. package/dist/query/relationship-options.js +1 -1
  159. package/dist/utils/password.d.ts +0 -68
  160. package/dist/utils/password.d.ts.map +1 -1
  161. package/dist/utils/password.js +7 -76
  162. package/dist/utils/password.js.map +1 -1
  163. package/dist/validation/field-config.d.ts.map +1 -1
  164. package/dist/validation/field-config.js +0 -6
  165. package/dist/validation/field-config.js.map +1 -1
  166. package/dist/validation/schema.d.ts +0 -7
  167. package/dist/validation/schema.d.ts.map +1 -1
  168. package/dist/validation/schema.js +0 -10
  169. package/dist/validation/schema.js.map +1 -1
  170. package/package.json +1 -1
  171. package/src/access/access-filter.test.ts +205 -275
  172. package/src/access/access-filter.ts +236 -272
  173. package/src/access/declared-dependencies.test.ts +277 -0
  174. package/src/access/declared-dependencies.ts +117 -37
  175. package/src/access/depth-limits.ts +8 -7
  176. package/src/access/engine.ts +0 -25
  177. package/src/access/errors.ts +97 -12
  178. package/src/access/field-access.test.ts +312 -2
  179. package/src/access/field-access.ts +118 -42
  180. package/src/access/field-transforms.ts +8 -39
  181. package/src/access/field-visibility.ts +99 -45
  182. package/src/access/index.ts +17 -5
  183. package/src/access/query-validation.ts +359 -0
  184. package/src/access/relationship-count.ts +3 -11
  185. package/src/access/relationship-label-filter.test.ts +19 -68
  186. package/src/access/relationship-label-filter.ts +27 -104
  187. package/src/access/transaction-registry.ts +58 -0
  188. package/src/access/types.ts +83 -86
  189. package/src/config/index.ts +10 -22
  190. package/src/config/label.ts +6 -13
  191. package/src/config/nav-count.ts +11 -19
  192. package/src/config/plugin-engine.ts +1 -37
  193. package/src/config/types.ts +95 -110
  194. package/src/context/apply-defaults.ts +12 -44
  195. package/src/context/hook-pipeline.ts +12 -67
  196. package/src/context/index.ts +311 -276
  197. package/src/context/nested-operations.ts +94 -226
  198. package/src/context/transaction-boundary.ts +113 -109
  199. package/src/context/write-pipeline.ts +99 -155
  200. package/src/fields/format-prisma-default.ts +4 -7
  201. package/src/fields/index.ts +155 -147
  202. package/src/filter/collect.ts +48 -12
  203. package/src/filter/filter.test.ts +153 -15
  204. package/src/filter/index.ts +1 -0
  205. package/src/filter/map.ts +0 -5
  206. package/src/filter/parse.ts +2 -9
  207. package/src/filter/serialize.ts +2 -4
  208. package/src/hooks/index.ts +5 -59
  209. package/src/index.ts +23 -4
  210. package/src/internal.ts +5 -0
  211. package/src/lib/case-utils.ts +0 -32
  212. package/src/mcp/handler.ts +13 -71
  213. package/src/query/index.ts +82 -235
  214. package/src/query/relationship-options.ts +1 -1
  215. package/src/utils/password.ts +7 -76
  216. package/src/validation/field-config.ts +0 -6
  217. package/src/validation/schema.ts +0 -10
  218. package/tests/access-relationships.test.ts +18 -16
  219. package/tests/computed-field-selective-evaluation.test.ts +418 -0
  220. package/tests/context.test.ts +727 -1
  221. package/tests/field-types.test.ts +222 -0
  222. package/tests/mcp-handler.test.ts +34 -0
  223. package/tests/needs-declared-dependencies.test.ts +7 -4
  224. package/tests/resolve-chain.test.ts +11 -11
  225. package/tests/transaction-boundary-outermost.test.ts +530 -0
  226. package/tsconfig.tsbuildinfo +1 -1
@@ -2,6 +2,7 @@ import { describe, test, expect } from 'vitest'
2
2
  import {
3
3
  text,
4
4
  integer,
5
+ bigInt,
5
6
  checkbox,
6
7
  timestamp,
7
8
  password,
@@ -327,6 +328,227 @@ describe('Field Types', () => {
327
328
  })
328
329
  })
329
330
 
331
+ describe('bigInt field', () => {
332
+ describe('getZodSchema', () => {
333
+ test('returns optional bigint schema for non-required field', () => {
334
+ const field = bigInt()
335
+ const schema = field.getZodSchema('epoch', 'create')
336
+
337
+ expect(schema.safeParse(25n).success).toBe(true)
338
+ expect(schema.safeParse(undefined).success).toBe(true)
339
+ })
340
+
341
+ test('returns required schema for required field in create mode', () => {
342
+ const field = bigInt({ validation: { isRequired: true } })
343
+ const schema = field.getZodSchema('epoch', 'create')
344
+
345
+ expect(schema.safeParse(25n).success).toBe(true)
346
+ expect(schema.safeParse(undefined).success).toBe(false)
347
+ })
348
+
349
+ test('returns optional schema for required field in update mode', () => {
350
+ const field = bigInt({ validation: { isRequired: true } })
351
+ const schema = field.getZodSchema('epoch', 'update')
352
+
353
+ expect(schema.safeParse(25n).success).toBe(true)
354
+ expect(schema.safeParse(undefined).success).toBe(true)
355
+ })
356
+
357
+ test('coerces a bigint input to a bigint output unchanged', () => {
358
+ const field = bigInt()
359
+ const schema = field.getZodSchema('epoch', 'create')
360
+
361
+ const result = schema.safeParse(9007199254740993n)
362
+ expect(result.success).toBe(true)
363
+ if (result.success) {
364
+ expect(result.data).toBe(9007199254740993n)
365
+ expect(typeof result.data).toBe('bigint')
366
+ }
367
+ })
368
+
369
+ test('coerces a safe-integer number input to a bigint output', () => {
370
+ const field = bigInt()
371
+ const schema = field.getZodSchema('epoch', 'create')
372
+
373
+ const result = schema.safeParse(1700000000000)
374
+ expect(result.success).toBe(true)
375
+ if (result.success) {
376
+ expect(result.data).toBe(1700000000000n)
377
+ expect(typeof result.data).toBe('bigint')
378
+ }
379
+ })
380
+
381
+ test('coerces a numeric string input to a bigint output, round-tripping beyond Number.MAX_SAFE_INTEGER', () => {
382
+ const field = bigInt()
383
+ const schema = field.getZodSchema('epoch', 'create')
384
+
385
+ const result = schema.safeParse('9223372036854775807')
386
+ expect(result.success).toBe(true)
387
+ if (result.success) {
388
+ expect(result.data).toBe(9223372036854775807n)
389
+ }
390
+ })
391
+
392
+ test('accepts a negative numeric string', () => {
393
+ const field = bigInt()
394
+ const schema = field.getZodSchema('delta', 'create')
395
+
396
+ const result = schema.safeParse('-42')
397
+ expect(result.success).toBe(true)
398
+ if (result.success) {
399
+ expect(result.data).toBe(-42n)
400
+ }
401
+ })
402
+
403
+ test('rejects a non-integer number', () => {
404
+ const field = bigInt()
405
+ const schema = field.getZodSchema('epoch', 'create')
406
+
407
+ expect(schema.safeParse(1.5).success).toBe(false)
408
+ })
409
+
410
+ test('rejects a number above Number.MAX_SAFE_INTEGER instead of silently losing precision', () => {
411
+ const field = bigInt()
412
+ const schema = field.getZodSchema('epoch', 'create')
413
+
414
+ expect(schema.safeParse(Number.MAX_SAFE_INTEGER + 10).success).toBe(false)
415
+ })
416
+
417
+ test('rejects a non-numeric string', () => {
418
+ const field = bigInt()
419
+ const schema = field.getZodSchema('epoch', 'create')
420
+
421
+ expect(schema.safeParse('not-a-number').success).toBe(false)
422
+ })
423
+
424
+ test('rejects a decimal string', () => {
425
+ const field = bigInt()
426
+ const schema = field.getZodSchema('epoch', 'create')
427
+
428
+ expect(schema.safeParse('1.5').success).toBe(false)
429
+ })
430
+
431
+ test('validates min value', () => {
432
+ const field = bigInt({ validation: { min: 0n } })
433
+ const schema = field.getZodSchema('epoch', 'create')
434
+
435
+ expect(schema.safeParse(-1n).success).toBe(false)
436
+ expect(schema.safeParse(0n).success).toBe(true)
437
+ expect(schema.safeParse(1n).success).toBe(true)
438
+ })
439
+
440
+ test('validates max value', () => {
441
+ const field = bigInt({ validation: { max: 100n } })
442
+ const schema = field.getZodSchema('epoch', 'create')
443
+
444
+ expect(schema.safeParse(99n).success).toBe(true)
445
+ expect(schema.safeParse(100n).success).toBe(true)
446
+ expect(schema.safeParse(101n).success).toBe(false)
447
+ })
448
+
449
+ test('validates min and max together', () => {
450
+ const field = bigInt({ validation: { min: 18n, max: 65n } })
451
+ const schema = field.getZodSchema('age', 'create')
452
+
453
+ expect(schema.safeParse(17n).success).toBe(false)
454
+ expect(schema.safeParse(18n).success).toBe(true)
455
+ expect(schema.safeParse(65n).success).toBe(true)
456
+ expect(schema.safeParse(66n).success).toBe(false)
457
+ })
458
+ })
459
+
460
+ describe('getPrismaType', () => {
461
+ test('returns BigInt type for basic field', () => {
462
+ const field = bigInt()
463
+ const prismaType = field.getPrismaType('epoch')
464
+
465
+ expect(prismaType.type).toBe('BigInt')
466
+ expect(prismaType.modifiers).toBe('?')
467
+ })
468
+
469
+ test('returns required BigInt for required field', () => {
470
+ const field = bigInt({ validation: { isRequired: true } })
471
+ const prismaType = field.getPrismaType('epoch')
472
+
473
+ expect(prismaType.type).toBe('BigInt')
474
+ expect(prismaType.modifiers).toBeUndefined()
475
+ })
476
+
477
+ test('db.isNullable: false makes field non-nullable regardless of validation', () => {
478
+ const field = bigInt({ db: { isNullable: false } })
479
+ const prismaType = field.getPrismaType('epoch')
480
+
481
+ expect(prismaType.modifiers).toBeUndefined()
482
+ })
483
+
484
+ test('db.map generates @map attribute', () => {
485
+ const field = bigInt({ db: { map: 'occurred_at_ms' } })
486
+ const prismaType = field.getPrismaType('epoch')
487
+
488
+ expect(prismaType.modifiers).toContain('@map("occurred_at_ms")')
489
+ })
490
+
491
+ test('defaultValue generates a bare @default literal', () => {
492
+ const field = bigInt({ defaultValue: 0n })
493
+ const prismaType = field.getPrismaType('epoch')
494
+
495
+ expect(prismaType.modifiers).toContain('@default(0)')
496
+ })
497
+
498
+ test('isIndexed: true requests a block-level index', () => {
499
+ const field = bigInt({ isIndexed: true })
500
+ const prismaType = field.getPrismaType('epoch')
501
+
502
+ expect(prismaType.index).toBe(true)
503
+ })
504
+
505
+ test("isIndexed: 'unique' generates @unique modifier", () => {
506
+ const field = bigInt({ isIndexed: 'unique' })
507
+ const prismaType = field.getPrismaType('epoch')
508
+
509
+ expect(prismaType.modifiers).toContain('@unique')
510
+ })
511
+ })
512
+
513
+ describe('getTypeScriptType', () => {
514
+ test('returns optional bigint type for non-required field', () => {
515
+ const field = bigInt()
516
+ const tsType = field.getTypeScriptType()
517
+
518
+ expect(tsType.type).toBe('bigint')
519
+ expect(tsType.optional).toBe(true)
520
+ })
521
+
522
+ test('returns required bigint type for required field', () => {
523
+ const field = bigInt({ validation: { isRequired: true } })
524
+ const tsType = field.getTypeScriptType()
525
+
526
+ expect(tsType.type).toBe('bigint')
527
+ expect(tsType.optional).toBe(false)
528
+ })
529
+ })
530
+
531
+ describe('getFilterSpec', () => {
532
+ test('parses a valid integer token into a BigInt condition', () => {
533
+ const field = bigInt()
534
+ const spec = field.getFilterSpec?.('epoch', 'Event', {} as never)
535
+
536
+ expect(spec?.toCondition('eq', '9007199254740993')).toEqual({
537
+ epoch: { equals: 9007199254740993n },
538
+ })
539
+ expect(spec?.toCondition('gt', '5')).toEqual({ epoch: { gt: 5n } })
540
+ })
541
+
542
+ test('degrades to free text (null) for a non-integer token', () => {
543
+ const field = bigInt()
544
+ const spec = field.getFilterSpec?.('epoch', 'Event', {} as never)
545
+
546
+ expect(spec?.toCondition('eq', '1.5')).toBeNull()
547
+ expect(spec?.toCondition('eq', 'not-a-number')).toBeNull()
548
+ })
549
+ })
550
+ })
551
+
330
552
  describe('checkbox field', () => {
331
553
  describe('getZodSchema', () => {
332
554
  test('returns optional boolean schema', () => {
@@ -326,6 +326,40 @@ describe('MCP Handler', () => {
326
326
  })
327
327
  })
328
328
 
329
+ // Regression test (issue #907 / ADR-0029): a bare `JSON.stringify` throws
330
+ // `TypeError: Do not know how to serialize a BigInt` on a bigInt() field's
331
+ // value. This must respond successfully, rendering the value as a decimal
332
+ // string — the field's MCP wire representation.
333
+ it('should serialize a bigint field value as a decimal string instead of throwing', async () => {
334
+ const mockResults = [{ id: '1', title: 'Post 1', occurredAtMs: 9007199254740993n }]
335
+ mockPrisma.post.findMany.mockResolvedValue(mockResults)
336
+
337
+ const handlers = createMcpHandlers({ config, getSession: mockGetSession, getContext })
338
+
339
+ const request = new Request('http://localhost/api/mcp', {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({
343
+ jsonrpc: '2.0',
344
+ id: 1,
345
+ method: 'tools/call',
346
+ params: {
347
+ name: 'list_post_query',
348
+ arguments: { where: {}, take: 10 },
349
+ },
350
+ }),
351
+ })
352
+
353
+ const response = await handlers.POST(request)
354
+ expect(response.status).toBe(200)
355
+
356
+ const data = await response.json()
357
+ expect(data.result.content[0].text).toContain('"occurredAtMs": "9007199254740993"')
358
+
359
+ const result = JSON.parse(data.result.content[0].text)
360
+ expect(result.items[0].occurredAtMs).toBe('9007199254740993')
361
+ })
362
+
329
363
  it('should execute create operation', async () => {
330
364
  const mockResult = { id: '1', title: 'New Post', content: 'New Content' }
331
365
  mockPrisma.post.create.mockResolvedValue(mockResult)
@@ -294,11 +294,14 @@ describe('a computed field declares the relations it needs (#850, ADR-0025)', ()
294
294
  expect(result).toHaveProperty('title')
295
295
  })
296
296
 
297
- it('a declaration cycle across two lists terminates via the existing relationship-graph cycle guard (ADR-0026 note)', async () => {
297
+ it("a declaration cycle across two lists terminates via the declaration fold's own cycle guard (ADR-0026 note)", async () => {
298
298
  // Order.total needs lineItems; LineItem.orderTitle needs order — a
299
- // two-list declaration cycle. This must not hang or crash: it rides the
300
- // SAME `visitedLists` cycle guard `buildIncludeWithAccessControl` already
301
- // uses for the relationship graph, not a separate mechanism.
299
+ // two-list declaration cycle. This must not hang or crash: it rides
300
+ // `foldDeclaredDependencies`'s own `visitedLists` cycle guard the same
301
+ // list-name-path mechanism the pre-ADR-0026 relationship-graph auto-walk
302
+ // used, re-pointed at the declaration fold now that nothing walks the
303
+ // relationship graph unprompted (defense in depth; the primary backstop
304
+ // is `validateNeedsClosureDepth` at generate time, see needs-closure.ts).
302
305
  const testConfig = await buildTestConfig({ withDeclarationCycle: true })
303
306
  const mockPrisma = createMockPrisma()
304
307
  mockPrisma.order.findMany.mockResolvedValue([
@@ -331,7 +331,7 @@ describe('resolve chain — concurrent hook invocations are isolated (#844)', ()
331
331
  expect(observedLengths.sort()).toEqual([1, 1, 1])
332
332
  })
333
333
 
334
- it('an unrelated top-level read in flight alongside a hook still gets its full explicit include scoped', async () => {
334
+ it('an unrelated top-level read in flight alongside a hook still gets its own explicit include scoped', async () => {
335
335
  let releaseSlowHook: () => void = () => {}
336
336
 
337
337
  const config: OpenSaasConfig = {
@@ -389,21 +389,21 @@ describe('resolve chain — concurrent hook invocations are isolated (#844)', ()
389
389
  await new Promise((resolve) => setTimeout(resolve, 0))
390
390
 
391
391
  // While the slow hook is still in flight, issue a plain top-level read
392
- // that has nothing to do with it. A bare read fetches scalars only
393
- // (ADR-0024), so name `child` explicitlya caller include naming a
394
- // relation BARE (`true`, no nested include of its own) still picks up the
395
- // access-controlled include for whatever lies beneath it.
396
- const fastPromise = context.db.fast.findMany({ include: { child: true } })
392
+ // that has nothing to do with it, explicitly naming a nested path two
393
+ // levels deep (child grandchild) the "One hop" rule (ADR-0026) means
394
+ // reaching grandchild requires naming it, which this request does.
395
+ const fastPromise = context.db.fast.findMany({
396
+ include: { child: { include: { grandchild: true } } },
397
+ })
397
398
 
398
399
  await fastPromise
399
400
  releaseSlowHook()
400
401
  await slowPromise
401
402
 
402
- // The unrelated read's access-controlled scoping must still descend two
403
- // levels deep (child grandchild), not collapse to a bare `{ child: true }`
404
- // because it happened to run while a totally different read's hook was
405
- // active — `insideResolveOutput` must stay scoped to the hook's OWN
406
- // derived context, never leak into a concurrently-running, unrelated one.
403
+ // The unrelated read's access-controlled scoping must descend exactly as
404
+ // far as ITS OWN request named, unaffected by a totally different read's
405
+ // hook being in flight concurrently no shared, leaking context state
406
+ // between the two.
407
407
  expect(prisma.fast.findMany.mock.calls[0][0].include).toEqual({
408
408
  child: { include: { grandchild: true } },
409
409
  })