@classytic/arc 2.11.4 → 2.14.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 (167) hide show
  1. package/README.md +16 -12
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/audit.d.mts +199 -0
  9. package/dist/auth/audit.mjs +288 -0
  10. package/dist/auth/index.d.mts +3 -3
  11. package/dist/auth/index.mjs +117 -191
  12. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  13. package/dist/buildHandler-olo-gt94.mjs +610 -0
  14. package/dist/cache/index.mjs +3 -3
  15. package/dist/cli/commands/describe.d.mts +89 -13
  16. package/dist/cli/commands/describe.mjs +56 -2
  17. package/dist/cli/commands/docs.mjs +2 -2
  18. package/dist/cli/commands/generate.mjs +147 -48
  19. package/dist/cli/commands/init.d.mts +13 -0
  20. package/dist/cli/commands/init.mjs +130 -87
  21. package/dist/cli/commands/introspect.mjs +8 -1
  22. package/dist/context/index.mjs +1 -1
  23. package/dist/core/index.d.mts +3 -3
  24. package/dist/core/index.mjs +5 -5
  25. package/dist/core-DECn6zaU.mjs +1399 -0
  26. package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CBxLLbn3.mjs} +7 -20
  27. package/dist/createAggregationRouter-CRIBv4sC.mjs +114 -0
  28. package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
  29. package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
  30. package/dist/docs/index.d.mts +24 -11
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  33. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  34. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  35. package/dist/errors-j4aJm1Wg.mjs +184 -0
  36. package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
  37. package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
  38. package/dist/events/index.d.mts +6 -6
  39. package/dist/events/index.mjs +11 -35
  40. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  41. package/dist/events/transports/redis.d.mts +1 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +2 -2
  44. package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
  45. package/dist/hooks/index.d.mts +1 -1
  46. package/dist/hooks/index.mjs +1 -1
  47. package/dist/idempotency/index.d.mts +1 -1
  48. package/dist/idempotency/index.mjs +1 -20
  49. package/dist/idempotency/redis.mjs +1 -1
  50. package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
  51. package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
  52. package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
  53. package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  54. package/dist/index.d.mts +6 -7
  55. package/dist/index.mjs +9 -10
  56. package/dist/integrations/event-gateway.d.mts +1 -1
  57. package/dist/integrations/event-gateway.mjs +1 -1
  58. package/dist/integrations/index.d.mts +1 -1
  59. package/dist/integrations/mcp/index.d.mts +2 -2
  60. package/dist/integrations/mcp/index.mjs +1 -1
  61. package/dist/integrations/mcp/testing.d.mts +1 -1
  62. package/dist/integrations/mcp/testing.mjs +1 -1
  63. package/dist/integrations/streamline.d.mts +60 -11
  64. package/dist/integrations/streamline.mjs +75 -85
  65. package/dist/integrations/websocket.mjs +2 -8
  66. package/dist/middleware/index.d.mts +1 -1
  67. package/dist/middleware/index.mjs +2 -2
  68. package/dist/migrations/index.d.mts +23 -3
  69. package/dist/migrations/index.mjs +0 -7
  70. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  71. package/dist/openapi-noXno2CV.mjs +968 -0
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/org/index.mjs +1 -1
  74. package/dist/permissions/index.d.mts +3 -3
  75. package/dist/permissions/index.mjs +3 -3
  76. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  77. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  78. package/dist/pipeline/index.d.mts +1 -1
  79. package/dist/pipeline/index.mjs +1 -1
  80. package/dist/plugins/index.d.mts +16 -31
  81. package/dist/plugins/index.mjs +33 -13
  82. package/dist/plugins/response-cache.mjs +1 -1
  83. package/dist/plugins/tracing-entry.mjs +1 -1
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +6 -9
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +1 -1
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +2 -2
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +6 -8
  92. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  93. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  94. package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
  95. package/dist/registry/index.d.mts +1 -1
  96. package/dist/registry/index.mjs +2 -2
  97. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  98. package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-DLL32us3.mjs} +224 -71
  99. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-DrOa-26E.mjs} +41 -36
  100. package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-lYhC2gE5.mjs} +1 -1
  101. package/dist/schemas/index.d.mts +100 -30
  102. package/dist/schemas/index.mjs +86 -29
  103. package/dist/scim/index.d.mts +264 -0
  104. package/dist/scim/index.mjs +963 -0
  105. package/dist/scope/index.d.mts +3 -3
  106. package/dist/scope/index.mjs +4 -4
  107. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  108. package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  109. package/dist/testing/index.d.mts +2 -8
  110. package/dist/testing/index.mjs +16 -24
  111. package/dist/types/index.d.mts +4 -4
  112. package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
  113. package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
  114. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  115. package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -2
  117. package/dist/utils/index.mjs +5 -5
  118. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  119. package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  120. package/package.json +24 -34
  121. package/skills/arc/SKILL.md +147 -51
  122. package/skills/arc/references/agent-auth.md +238 -0
  123. package/skills/arc/references/api-reference.md +187 -0
  124. package/skills/arc/references/auth.md +354 -7
  125. package/skills/arc/references/enterprise-auth.md +94 -0
  126. package/skills/arc/references/events.md +8 -6
  127. package/skills/arc/references/mcp.md +2 -2
  128. package/skills/arc/references/multi-tenancy.md +11 -2
  129. package/skills/arc/references/production.md +10 -9
  130. package/skills/arc/references/scim.md +247 -0
  131. package/skills/arc/references/testing.md +1 -1
  132. package/skills/arc-code-review/SKILL.md +141 -0
  133. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  134. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  135. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  136. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  137. package/skills/arc-code-review/references/scaffolding.md +230 -0
  138. package/skills/arc-code-review/references/severity.md +127 -0
  139. package/dist/EventTransport-BFQjw9pB.mjs +0 -133
  140. package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
  141. package/dist/adapters/index.d.mts +0 -3
  142. package/dist/adapters/index.mjs +0 -2
  143. package/dist/adapters-DUUiiimH.mjs +0 -964
  144. package/dist/auth/mongoose.d.mts +0 -191
  145. package/dist/auth/mongoose.mjs +0 -73
  146. package/dist/core-CbcQRIch.mjs +0 -1054
  147. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  148. package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
  149. package/dist/errors-D5c-5BJL.mjs +0 -232
  150. package/dist/index-Rg8axYPz.d.mts +0 -370
  151. package/dist/openapi-D7G1V7ex.mjs +0 -557
  152. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  153. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  154. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  155. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  156. /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  157. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  158. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  159. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  160. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  161. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
  162. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  163. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  164. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  165. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  166. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  167. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
@@ -0,0 +1,911 @@
1
+ # Arc Anti-Pattern Detection Catalog
2
+
3
+ Every entry below has: **detection** (greppable regex / file pattern), **severity**, **why it matters**, and **fix** (with arc API citation). Run each detection against `src/` (excluding `node_modules`, `dist`, `coverage`, `*.test.*`).
4
+
5
+ Severity legend: 🔴 Critical · 🟠 High · 🟡 Medium · 🟢 Low
6
+
7
+ ---
8
+
9
+ ## §1. Manual query parsing 🟡
10
+
11
+ **Detection (Grep):**
12
+ ```
13
+ pattern: "req\\.query\\.(filter|sort|page|limit|search|q)\\b"
14
+ glob: "**/*.{ts,js}"
15
+ output_mode: content
16
+ ```
17
+ Also: `\\$or\\s*:\\s*\\[`, `\\$and\\s*:\\s*\\[`, `\\$regex`, `Number\\(req\\.query\\.`, `parseInt\\(req\\.query\\.`.
18
+
19
+ **Anti-pattern:**
20
+ ```typescript
21
+ const filters: any = {};
22
+ if (req.query.status) filters.status = req.query.status;
23
+ if (req.query.minPrice) filters.price = { $gte: Number(req.query.minPrice) };
24
+ if (req.query.q) {
25
+ filters.$or = [
26
+ { name: { $regex: req.query.q, $options: 'i' } },
27
+ { description: { $regex: req.query.q, $options: 'i' } },
28
+ ];
29
+ }
30
+ const items = await Product.find(filters)
31
+ .skip((Number(req.query.page) - 1) * 20)
32
+ .limit(20);
33
+ ```
34
+
35
+ **Why critical-adjacent:** ad-hoc parsing is a ReDoS vector (no regex bounds), bypasses field whitelists, and silently diverges across resources.
36
+
37
+ **Fix:** Arc's built-in parser handles filters, operators, sort, pagination, populate, select.
38
+ ```typescript
39
+ defineResource({
40
+ name: 'product',
41
+ schemaOptions: {
42
+ query: {
43
+ filterableFields: { status: { type: 'string' }, price: { type: 'number' } },
44
+ allowedPopulate: ['category'],
45
+ },
46
+ },
47
+ });
48
+ // GET /products?status=active&price[gte]=100&sort=-createdAt&page=2&limit=20&populate=category
49
+ ```
50
+ For MongoDB-specific operators (`$lookup`, geo): use `QueryParser` from `@classytic/mongokit`:
51
+ ```typescript
52
+ import { QueryParser } from '@classytic/mongokit';
53
+ defineResource({
54
+ queryParser: new QueryParser({
55
+ allowedFilterFields: ['status', 'category'],
56
+ allowedSortFields: ['createdAt', 'price'],
57
+ allowedOperators: ['eq', 'gte', 'lte', 'in'],
58
+ }),
59
+ });
60
+ ```
61
+ ReDoS protection, max-depth, max-limit are enforced.
62
+
63
+ ---
64
+
65
+ ## §2. Hand-written Fastify route schemas 🟡
66
+
67
+ **Detection:**
68
+ ```
69
+ pattern: "fastify\\.(get|post|patch|put|delete)\\([^,]+,\\s*\\{\\s*schema\\s*:"
70
+ multiline: true
71
+ ```
72
+ Or: `properties\\s*:\\s*\\{` near `route\\(|fastify\\.(get|post|patch)`.
73
+
74
+ **Anti-pattern:**
75
+ ```typescript
76
+ fastify.post('/products', {
77
+ schema: {
78
+ body: {
79
+ type: 'object',
80
+ properties: {
81
+ name: { type: 'string', minLength: 1 },
82
+ price: { type: 'number', minimum: 0 },
83
+ },
84
+ required: ['name', 'price'],
85
+ },
86
+ response: { 200: { type: 'object', properties: { id: {...}, name: {...} } } },
87
+ },
88
+ handler: async (req) => Product.create(req.body),
89
+ });
90
+ ```
91
+
92
+ **Why:** Schema duplicates Mongoose model + diverges over time. No OpenAPI sync.
93
+
94
+ **Fix:**
95
+ ```typescript
96
+ defineResource({
97
+ name: 'product',
98
+ schemaOptions: {
99
+ fieldRules: {
100
+ name: { type: 'string', minLength: 1, required: true },
101
+ price: { type: 'number', minimum: 0, required: true },
102
+ },
103
+ },
104
+ });
105
+ ```
106
+ Body, response, and OpenAPI auto-derive. Mongoose model constraints take precedence; `fieldRules` supplements.
107
+
108
+ ---
109
+
110
+ ## §3. Manual CRUD routes 🟠
111
+
112
+ **Detection:** Multiple `fastify.(get|post|patch|delete)` calls for the same path stem.
113
+ ```
114
+ pattern: "fastify\\.(get|post|patch|put|delete)\\(['\"]\\/(\\w+)"
115
+ output_mode: content
116
+ ```
117
+ Group hits by capture group 2 (path stem). Three or more methods on the same stem ⇒ candidate for `defineResource`.
118
+
119
+ **Anti-pattern:** A `routes/products.ts` file with `GET /products`, `GET /products/:id`, `POST /products`, `PATCH /products/:id`, `DELETE /products/:id` each implemented inline.
120
+
121
+ **Why high:** One resource gets a fix (e.g., pagination, error handling) and the others drift. Arc generates all five identically.
122
+
123
+ **Fix:** Replace the whole file with one `defineResource`. See [migration-recipes.md §1](migration-recipes.md).
124
+
125
+ ---
126
+
127
+ ## §4. Manual permission checks inside handlers 🔴
128
+
129
+ **Detection:**
130
+ ```
131
+ pattern: "(req|request)\\.user\\.(role|roles)\\b|user\\.role\\s*[!=]==|roles\\.includes\\("
132
+ output_mode: content
133
+ ```
134
+ Also: `if\\s*\\(\\s*!\\s*req\\.user\\s*\\)`, `throw\\s+new\\s+Error\\(['\"](Unauthorized|Forbidden)`.
135
+
136
+ **Anti-pattern:**
137
+ ```typescript
138
+ fastify.post('/products', async (req, reply) => {
139
+ if (!req.user) throw new Error('Unauthorized'); // 401 hand-rolled
140
+ if (!req.user.roles.includes('admin')) throw new Error('Forbidden'); // 403 hand-rolled
141
+ if (product.createdBy !== req.user.id) throw new Error('Not yours'); // ownership
142
+ // proceed
143
+ });
144
+ ```
145
+
146
+ **Why critical:**
147
+ - Inconsistent error shape (Error vs ArcError). Frontend gets unpredictable bodies.
148
+ - `req.user.id` vs `req.user._id` mismatches between routes silently bypass ownership.
149
+ - No row-level filter — admin sees `Product.find()` with no tenant scope.
150
+
151
+ **Fix:** Declarative permission combinators. Permissions run at framework level, `filters` propagate into the repo query (row-level ABAC):
152
+ ```typescript
153
+ import {
154
+ allowPublic, requireAuth, requireRoles, requireOwnership,
155
+ requireOrgMembership, requireOrgRole, requireServiceScope,
156
+ allOf, anyOf,
157
+ } from '@classytic/arc';
158
+
159
+ defineResource({
160
+ permissions: {
161
+ list: allowPublic(),
162
+ get: allowPublic(),
163
+ create: requireRoles(['admin', 'editor']),
164
+ update: anyOf(requireRoles(['admin']), requireOwnership('createdBy')),
165
+ delete: requireRoles(['admin']),
166
+ },
167
+ });
168
+ ```
169
+ For mixed human + service: `anyOf(requireOrgRole('admin'), requireServiceScope('jobs:bulk-write'))`.
170
+
171
+ ---
172
+
173
+ ## §5. Manual `toJSON` transforms / response field stripping 🔴
174
+
175
+ **Detection:**
176
+ ```
177
+ pattern: "schema\\.set\\(['\"]toJSON['\"]|toJSON\\s*=\\s*function|delete\\s+(ret|obj|doc)\\.(password|__v|secret)"
178
+ output_mode: content
179
+ ```
180
+ Also: `onSerialization` Fastify hook bodies that delete fields.
181
+
182
+ **Anti-pattern:**
183
+ ```typescript
184
+ userSchema.set('toJSON', {
185
+ transform: (_doc, ret) => {
186
+ delete ret.password;
187
+ delete ret.__v;
188
+ delete ret.secretToken;
189
+ return ret;
190
+ },
191
+ });
192
+ ```
193
+
194
+ **Why critical:** Easy to forget on a new field; password leaks happen this way. Also breaks lean reads (the transform doesn't fire on `.lean()` results).
195
+
196
+ **Fix:** `fieldRules.hidden` (or field-level read permission) — applies at framework serialization for both REST and MCP, and works with lean reads:
197
+ ```typescript
198
+ import { fields } from '@classytic/arc';
199
+
200
+ defineResource({
201
+ schemaOptions: {
202
+ fieldRules: {
203
+ password: { hidden: true },
204
+ __v: { hidden: true },
205
+ secretToken: { hidden: true },
206
+ salary: fields.visibleTo(['admin', 'hr']),
207
+ email: fields.redactFor(['viewer'], '***'),
208
+ },
209
+ },
210
+ });
211
+ ```
212
+
213
+ ---
214
+
215
+ ## §6. Hand-maintained OpenAPI / Swagger 🟡
216
+
217
+ **Detection:**
218
+ - Files: `openapi.yaml`, `openapi.json`, `swagger.yaml`, `swagger.json`, `api-spec.*` checked into the repo.
219
+ - `@fastify/swagger` registered with hand-written `swagger.definitions`.
220
+
221
+ **Why medium:** Spec drifts from code; consumers integrate against stale docs.
222
+
223
+ **Fix:** `arc docs ./openapi.json --entry ./dist/index.js` — emits OpenAPI from registered resources. Wire into `prebuild` or CI.
224
+ ```typescript
225
+ import { SpecGenerator } from '@classytic/arc/docs';
226
+ const spec = await app.arc.docs.getSpec({ title: 'My API', version: '1.0.0' });
227
+ ```
228
+
229
+ ---
230
+
231
+ ## §7. Manual event emission 🟠
232
+
233
+ **Detection:**
234
+ ```
235
+ pattern: "(eventBus|emitter|events|pubsub)\\.(emit|publish)\\(['\"](\\w+)\\.(created|updated|deleted)"
236
+ output_mode: content
237
+ ```
238
+
239
+ **Anti-pattern:**
240
+ ```typescript
241
+ const product = await Product.create(req.body);
242
+ await eventBus.emit('product.created', { id: product._id });
243
+ // ... and somebody forgets it on PATCH
244
+ ```
245
+
246
+ **Why high:** Inconsistent emission breaks downstream consumers (search index, audit log, cache invalidation). Test coverage rarely catches missing emits.
247
+
248
+ **Fix:** Arc auto-emits `{resource}.created` / `.updated` / `.deleted` on every CRUD. Custom events go through hooks:
249
+ ```typescript
250
+ defineResource({
251
+ name: 'product',
252
+ events: {
253
+ created: { description: 'Product created' },
254
+ priceChanged: { description: 'Price updated', schema: { oldPrice, newPrice } },
255
+ },
256
+ hooks: {
257
+ afterUpdate: async (ctx) => {
258
+ if (ctx.data.price !== ctx.meta?.existing?.price) {
259
+ await ctx.fastify.events.publish('product.priceChanged', {
260
+ id: ctx.data._id,
261
+ oldPrice: ctx.meta.existing.price,
262
+ newPrice: ctx.data.price,
263
+ });
264
+ }
265
+ },
266
+ },
267
+ });
268
+ ```
269
+ For guaranteed delivery, use `EventOutbox` (transactional outbox pattern).
270
+
271
+ ---
272
+
273
+ ## §8. Manual cache invalidation 🟡
274
+
275
+ **Detection:**
276
+ ```
277
+ pattern: "(redis|cache)\\.(del|invalidate|set)\\(['\"](\\w+)[-:_]"
278
+ output_mode: content
279
+ ```
280
+ Also: hand-rolled cache key strings.
281
+
282
+ **Anti-pattern:**
283
+ ```typescript
284
+ const product = await Product.create(req.body);
285
+ await cache.del(`product-${product._id}`);
286
+ await cache.del('products-list'); // forgot the org-scoped key
287
+ ```
288
+
289
+ **Fix:** Arc's `QueryCache` invalidates by tag on every mutation. Enable via `arcPlugins.queryCache: true` and declare tags per resource:
290
+ ```typescript
291
+ defineResource({
292
+ cache: {
293
+ staleTime: 30,
294
+ gcTime: 300,
295
+ tags: ['catalog'],
296
+ invalidateOn: { 'category.*': ['catalog'] }, // cross-resource
297
+ },
298
+ });
299
+ ```
300
+ Modes: `memory` (default) / `distributed` (requires `stores.queryCache: RedisCacheStore`). Response header: `x-cache: HIT | STALE | MISS`.
301
+
302
+ ---
303
+
304
+ ## §9. Bypassing RequestScope 🔴
305
+
306
+ **Detection:**
307
+ ```
308
+ pattern: "req(uest)?\\.user\\.(_id|id|orgId|organizationId|tenantId|parentOrgId|teamId)"
309
+ output_mode: content
310
+ ```
311
+
312
+ **Anti-pattern:**
313
+ ```typescript
314
+ const userId = req.user._id; // crashes on public route
315
+ const orgId = req.user.orgId; // undefined for service tokens
316
+ const parent = req.user.parentOrgId; // not real — fragile custom field
317
+ ```
318
+
319
+ **Why critical:**
320
+ - Crashes on public routes (`request.user` is `Record<string, unknown> | undefined`).
321
+ - Doesn't differentiate `member` vs `service` vs `elevated` scope kinds.
322
+ - Org hierarchy not honored (no ancestor lookup).
323
+ - Inconsistent across routes.
324
+
325
+ **Fix:** Always use accessors from `@classytic/arc/scope`:
326
+ ```typescript
327
+ import {
328
+ getUserId, getOrgId, getOrgRoles, getServiceScopes,
329
+ getScopeContext, getAncestorOrgIds,
330
+ isMember, isService, isElevated, isAuthenticated, hasOrgAccess,
331
+ isOrgInScope,
332
+ } from '@classytic/arc/scope';
333
+
334
+ if (!isAuthenticated(req.scope)) return reply.unauthorized();
335
+ const userId = getUserId(req.scope); // typed, undefined-safe
336
+ const orgId = getOrgId(req.scope);
337
+ const branch = getScopeContext(req.scope, 'branchId');
338
+ const inOrg = isOrgInScope(req.scope, 'acme-eu');
339
+ ```
340
+ For permissions, prefer combinators (`requireOrgRole`, `requireScopeContext`, `requireOrgInScope`) over accessor logic in handlers.
341
+
342
+ ---
343
+
344
+ ## §10. Mongoose/Prisma imports outside adapters 🟠
345
+
346
+ **Detection:**
347
+ ```
348
+ pattern: "^import\\s+.*\\b(mongoose|@prisma/client|drizzle-orm)\\b"
349
+ glob: "src/**/*.{ts,js}"
350
+ output_mode: content
351
+ ```
352
+ Then exclude allowed paths: `src/**/adapter*.ts` (host-side adapter file), `src/db/**` (depending on convention), and `src/**/*.model.ts` (model definition is OK).
353
+
354
+ **Anti-pattern:** `import mongoose from 'mongoose'` in `src/services/`, `src/hooks/`, `src/routes/`, or any `.resource.ts`.
355
+
356
+ **Why high:** Defeats arc's DB-agnostic boundary. Forces every consumer of that file to pull a database driver. Migration to a different DB becomes a multi-thousand-LOC rewrite instead of swapping one adapter file.
357
+
358
+ **Fix:** Mongoose calls go in:
359
+ 1. `*.model.ts` — schema definition only.
360
+ 2. `*.repository.ts` — extends `Repository<TDoc>` from `@classytic/mongokit` (or implements `MinimalRepo`).
361
+ 3. `*.adapter.ts` — `createMongooseAdapter({ model, repository })`.
362
+
363
+ Resources, hooks, and services consume `RepositoryLike<TDoc>` — never the model directly.
364
+
365
+ ---
366
+
367
+ ## §11. `console.log` in `src/` 🟢
368
+
369
+ **Detection:**
370
+ ```
371
+ pattern: "console\\.(log|warn|error|info|debug)\\("
372
+ glob: "src/**/*.{ts,js}"
373
+ output_mode: content
374
+ ```
375
+ Allowed: `src/cli/**`, scripts, examples.
376
+
377
+ **Fix:** Use Fastify's logger via `request.log` / `app.log`, or arc's logger if exposed.
378
+
379
+ ---
380
+
381
+ ## §12. `any` and `@ts-ignore` 🟢
382
+
383
+ **Detection:**
384
+ ```
385
+ pattern: ":\\s*any\\b|<any>|as\\s+any\\b|@ts-ignore|@ts-expect-error"
386
+ glob: "src/**/*.ts"
387
+ ```
388
+
389
+ **Fix:** Use `unknown` + type narrowing. `as unknown as X` is a last resort, not a shortcut. Never `@ts-ignore` — fix the type. For repo casts, prefer `RepositoryLike<TDoc>` from `@classytic/arc`.
390
+
391
+ ---
392
+
393
+ ## §13. Default exports 🟢
394
+
395
+ **Detection:**
396
+ ```
397
+ pattern: "^export\\s+default\\b"
398
+ glob: "src/**/*.ts"
399
+ ```
400
+
401
+ **Fix:** Named exports only. `export const productResource = defineResource({...})`. Knip enforces in arc itself; client projects should follow.
402
+
403
+ **Exception:** `*.resource.ts` files used by `loadResources()` may use `export default` because the loader recognizes both `default` export AND `export const resource`. Prefer named for grep-ability.
404
+
405
+ ---
406
+
407
+ ## §14. Reimplementing presets manually 🟡
408
+
409
+ **Detection:** Look for handler names + route paths that match preset signatures:
410
+
411
+ | Preset | Handler / route fingerprint |
412
+ |---|---|
413
+ | `softDelete` | `getDeleted`, `restore`, `GET /deleted`, `POST /:id/restore`, field `deletedAt` with manual filtering on every read |
414
+ | `slugLookup` | `getBySlug`, `GET /slug/:slug`, `GET /by-slug/:slug` |
415
+ | `tree` | `getChildren`, `getTree`, `GET /tree`, `GET /:id/children`, manual `parentId` field |
416
+ | `bulk` | `bulkCreate`, `bulkUpdate`, `bulkDelete`, `POST /bulk`, `PATCH /bulk`, `DELETE /bulk` |
417
+ | `multiTenant` | Every find call appended with `organizationId: req.user.orgId` |
418
+ | `ownedByUser` | Every find call appended with `userId: req.user.id` |
419
+ | `audited` | Manual write to an audit collection on every mutation |
420
+ | `filesUpload` | `multer`, `@fastify/multipart` ad-hoc routes per resource |
421
+ | `search` | `POST /search`, `/search-similar`, `/embed` hand-rolled |
422
+
423
+ **Fix:**
424
+ ```typescript
425
+ defineResource({
426
+ presets: [
427
+ 'softDelete',
428
+ { name: 'multiTenant', tenantField: 'organizationId' },
429
+ { name: 'slugLookup', slugField: 'slug' },
430
+ 'bulk',
431
+ { name: 'filesUpload', storage: s3Storage, allowedMimeTypes: ['image/*'] },
432
+ ],
433
+ });
434
+ ```
435
+ Multi-level tenancy (branch/project/region) — use `multiTenantPreset({ tenantFields: TenantFieldSpec[] })`.
436
+
437
+ ---
438
+
439
+ ## §15. Hand-written MCP tools 🟡
440
+
441
+ **Detection:**
442
+ ```
443
+ pattern: "CallToolRequestSchema|setRequestHandler\\(\\s*Call|inputSchema\\s*:\\s*\\{\\s*type\\s*:\\s*['\"]object"
444
+ output_mode: content
445
+ ```
446
+ Or: a file pattern of `tools.ts`/`mcp.ts` defining `name`, `description`, `inputSchema`, `handler` objects manually.
447
+
448
+ **Fix:** Resources auto-generate MCP tools (5 CRUD + custom routes + actions). Permissions and field rules carry over.
449
+ ```typescript
450
+ import { mcpPlugin } from '@classytic/arc/mcp';
451
+
452
+ await app.register(mcpPlugin, {
453
+ resources: [productResource, orderResource],
454
+ auth: false, // or getAuth() / custom
455
+ exclude: ['credential'],
456
+ overrides: { product: { operations: ['list', 'get'] } },
457
+ });
458
+ ```
459
+ For domain-specific tools, use `extraTools` + `buildMcpToolsFromBridges([...])` instead of hand-rolling MCP server plumbing.
460
+
461
+ ---
462
+
463
+ ## §16. Custom search routes / query language 🟡
464
+
465
+ **Detection:** `GET /search`, `GET /:resource/search`, `/find`, `/lookup` with custom param parsing.
466
+
467
+ **Fix:** Either use arc's standard `QueryParser` syntax (`?search=...&filter=...`), or `searchPreset` for full-text/vector backends:
468
+ ```typescript
469
+ import { searchPreset } from '@classytic/arc/presets/search';
470
+
471
+ defineResource({
472
+ presets: [searchPreset({
473
+ search: { handler: (req) => elastic.search({ index: 'products', q: req.body.q }) },
474
+ similar: { handler: (req) => pinecone.query({ vector: req.body.vector, topK: 10 }) },
475
+ })],
476
+ });
477
+ ```
478
+
479
+ ---
480
+
481
+ ## §17. `request.user` access without guard on public routes 🔴
482
+
483
+ **Detection:**
484
+ ```
485
+ pattern: "(req|request)\\.user\\.\\w+"
486
+ output_mode: content
487
+ ```
488
+ Then for each hit, check whether the enclosing route is `allowPublic()` or has no `permissions`. Crashes happen on public routes.
489
+
490
+ **Fix:** Always guard:
491
+ ```typescript
492
+ if (req.user) {
493
+ const userId = req.user.id;
494
+ // ...
495
+ }
496
+ // or: use scope accessors which return undefined safely
497
+ ```
498
+ Arc rule: `request.user: Record<string, unknown> | undefined`. The property is required (not optional `?:`), but the value is union-with-undefined.
499
+
500
+ ---
501
+
502
+ ## §18. Custom controller without forwarding `tenantField`/`schemaOptions`/`idField` 🟠
503
+
504
+ **Detection:** A `class FooController extends BaseController` where the constructor doesn't forward `tenantField`, `idField`, `schemaOptions`, `cache`, `onFieldWriteDenied`.
505
+
506
+ **Why:** When you pass your own controller to `defineResource({ controller })`, arc CANNOT thread these into it. They must be forwarded through `super(repo, opts)` AND mirrored on the `defineResource` call.
507
+
508
+ **Fix:**
509
+ ```typescript
510
+ class ProductController extends BaseController<Product> {
511
+ constructor(opts: { tenantField?: string | false; idField?: string } = {}) {
512
+ super(productRepo, { resourceName: 'product', ...opts });
513
+ }
514
+ }
515
+
516
+ defineResource({
517
+ name: 'product',
518
+ controller: new ProductController({ tenantField: 'organizationId', idField: '_id' }),
519
+ tenantField: 'organizationId', // mirror!
520
+ idField: '_id', // mirror!
521
+ });
522
+ ```
523
+ Or skip the custom controller entirely — arc's auto-built `BaseController` handles 90% of cases. Use `BaseCrudController` + `SoftDeleteMixin`/`BulkMixin`/etc. for slim CRUD-only.
524
+
525
+ ---
526
+
527
+ ## §19. `tenantField` left at default for company-wide tables 🟠
528
+
529
+ **Detection:** Resources for lookup tables, platform settings, or cross-org reports that don't set `tenantField: false`.
530
+
531
+ **Why:** Default `tenantField: 'organizationId'` silently scopes queries to caller's org. For an `account-type` lookup table or `currency` table, this returns empty results forever.
532
+
533
+ **Fix:**
534
+ ```typescript
535
+ defineResource({ name: 'account-type', tenantField: false }); // company-wide
536
+ defineResource({ name: 'workspace-item', tenantField: 'workspaceId' }); // different scope
537
+ ```
538
+
539
+ ---
540
+
541
+ ## §20. Wrong lifecycle slot for async-booted engines 🟠
542
+
543
+ **Detection:** A resource adapter that depends on an engine initialized inside the same module (e.g., `await ensureCatalogEngine()`) is exported as a static `defineResource()` at module top — which runs before `bootstrap[]`.
544
+
545
+ **Anti-pattern:**
546
+ ```typescript
547
+ // product.resource.ts — runs at IMPORT time, before bootstrap
548
+ const engine = await ensureCatalogEngine(); // top-level await
549
+ export const productResource = defineResource({ adapter: engine.adapter() });
550
+ ```
551
+
552
+ **Fix:** Use the factory form for `resources` so it runs *after* `bootstrap[]`:
553
+ ```typescript
554
+ const app = await createApp({
555
+ bootstrap: [async () => { await ensureCatalogEngine(); }],
556
+ resources: async () => {
557
+ const engine = await getCatalogEngine();
558
+ return [buildProductResource(engine), buildCategoryResource(engine)];
559
+ },
560
+ });
561
+ ```
562
+ Or via `loadResources` with `context`:
563
+ ```typescript
564
+ resources: async () => loadResources(import.meta.url, { context: { engine } }),
565
+ // Each *.resource.ts then exports: (ctx) => defineResource({ adapter: ctx.engine.adapter() })
566
+ ```
567
+
568
+ ---
569
+
570
+ ## §21. Field-write denied as silent strip when reject is needed (or vice versa) 🟡
571
+
572
+ **Detection:** Resources with sensitive fields (`role`, `permissions`, `tenantId`, `organizationId`) that don't declare `onFieldWriteDenied`. Default is `reject` (403 with denied list) — usually correct. If client set `'strip'`, sensitive denied writes silently disappear.
573
+
574
+ **Fix:** Explicit choice per resource:
575
+ ```typescript
576
+ defineResource({ onFieldWriteDenied: 'reject' }); // default — explicit
577
+ defineResource({ onFieldWriteDenied: 'strip' }); // only for low-stakes UX flows
578
+ ```
579
+ Tests should assert 403 on protected-field write attempts.
580
+
581
+ ---
582
+
583
+ ## §22. Manual idempotency 🟠
584
+
585
+ **Detection:** Hand-rolled deduplication via `Idempotency-Key` header reads, custom Redis/Mongo lookup, retry tables.
586
+
587
+ **Fix:** Arc ships `idempotencyPlugin`:
588
+ ```typescript
589
+ import { idempotencyPlugin } from '@classytic/arc/idempotency';
590
+ await app.register(idempotencyPlugin, { repository: idempotencyRepo, ttlMs: 24 * 3600_000 });
591
+ ```
592
+ Requires `getOne`, `deleteMany`, `findOneAndUpdate` on the repo (mongokit + sqlitekit conform).
593
+
594
+ ---
595
+
596
+ ## §23. Manual audit log 🟡
597
+
598
+ **Detection:** Every mutation handler appends to an `audit_logs` / `events` / `history` collection inline.
599
+
600
+ **Fix:** Per-resource opt-in via `auditPlugin`:
601
+ ```typescript
602
+ await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
603
+ defineResource({ name: 'order', audit: true });
604
+ defineResource({ name: 'payment', audit: { operations: ['delete'] } });
605
+ ```
606
+ Or `presets: ['audited']`.
607
+
608
+ ---
609
+
610
+ ## §24. Manual rate limiting per route 🟡
611
+
612
+ **Detection:** `@fastify/rate-limit` registered per-route with hand-tuned configs.
613
+
614
+ **Fix:** Per-resource `rateLimit: RateLimitConfig` on `defineResource`, or global at `createApp({ rateLimit })`.
615
+
616
+ ---
617
+
618
+ ## §25. Webhook signature verification on parsed body 🔴
619
+
620
+ **Detection:**
621
+ ```
622
+ pattern: "verifySignature\\(.*req\\.body"
623
+ output_mode: content
624
+ ```
625
+
626
+ **Why critical:** `verifySignature(body, ...)` throws `TypeError` if body isn't string/Buffer. Parsed body silently fails verification → forged webhooks accepted.
627
+
628
+ **Fix:** `verifySignature(req.rawBody, ...)`. Mark webhook routes `raw: true` and ensure `@fastify/raw-body` is registered (arc registers it automatically when needed).
629
+
630
+ ---
631
+
632
+ ## §26. Multipart/file upload handled with multer or ad-hoc parsing 🟡
633
+
634
+ **Detection:** `multer`, `formidable`, `busboy` used directly; or `req.body` consumed without `multipartBody` middleware.
635
+
636
+ **Fix:**
637
+ ```typescript
638
+ import { multipartBody } from '@classytic/arc/middleware';
639
+ defineResource({
640
+ middlewares: { create: [multipartBody({ allowedMimeTypes: ['image/png'], maxFileSize: 5 * 1024 * 1024 })] },
641
+ });
642
+ ```
643
+ Or `presets: [{ name: 'filesUpload', storage, allowedMimeTypes, maxFileSize }]`.
644
+ `multipartBody()` is a no-op for JSON requests — safe to always add to create/update.
645
+
646
+ ---
647
+
648
+ ## §27. SSE auth via query string without `preAuth` 🔴
649
+
650
+ **Detection:** SSE/EventSource routes that read `req.query.token` and call `verify()` inline.
651
+
652
+ **Fix:** EventSource can't set headers, so arc's `preAuth` slot copies `?token=` into the `Authorization` header before auth runs:
653
+ ```typescript
654
+ routes: [
655
+ {
656
+ method: 'GET', path: '/stream', raw: true,
657
+ preAuth: [(req) => { req.headers.authorization = `Bearer ${req.query.token}`; }],
658
+ permissions: requireAuth(),
659
+ handler: async (req, reply) => reply.send(stream),
660
+ },
661
+ ],
662
+ ```
663
+
664
+ ---
665
+
666
+ ## §28. Mongoose without mongokit 🟠
667
+
668
+ **Detection:** `package.json` has `mongoose` but not `@classytic/mongokit`. Or `import { Schema, model } from 'mongoose'` paired with hand-rolled repository classes (`class FooRepository`).
669
+
670
+ **Why high:** Every Mongoose model + repo class is duplicating mongokit's hook engine, plugin system, pagination, multi-tenant scoping, soft-delete, audit trail, transaction helpers. A 150 LOC repo class typically reduces to ~30 LOC with mongokit.
671
+
672
+ **Fix:** Full migration recipe → [mongokit-migration.md](mongokit-migration.md).
673
+
674
+ ---
675
+
676
+ ## §29. `app.register(authPlugin)` instead of `createApp({ auth })` 🟢
677
+
678
+ **Detection:** Manual JWT plugin registration with `@fastify/jwt`, hand-rolled `app.authenticate` decorator.
679
+
680
+ **Fix:** `createApp({ auth: { type: 'jwt', jwt: { secret, ... } } })` decorates `app.authenticate`, `app.optionalAuthenticate`, `app.authorize` and wires permission checks. For Better Auth, `createApp({ auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth, orgContext: true }) } })`.
681
+
682
+ ---
683
+
684
+ ## §30. Missing `arc-discovered` resources because of tsconfig path aliases 🟢
685
+
686
+ **Detection:** Resources defined under `import.meta.url`-discovered directories don't appear in routes; resource files import other resource files via `@/foo` aliases.
687
+
688
+ **Why:** `loadResources()` uses Node's resolver. `@/*` path aliases are compile-time only, not Node-resolvable. Auto-discovery silently misses these files.
689
+
690
+ **Fix:** Use relative imports OR Node's `#` subpath imports (`"imports": { "#foo/*": "./src/foo/*.js" }` in `package.json`) for files that participate in `loadResources`. Path aliases work fine for type-only imports.
691
+
692
+ ---
693
+
694
+ ## §31. `select` field manipulated client-side 🟢
695
+
696
+ **Detection:** Code post-processing `select` value from query (normalizing case, splitting, building projection objects).
697
+
698
+ **Why:** Arc preserves `select` as-is for DB-agnosticism (Mongo `'name email'` vs SQL `['name', 'email']`). Client normalization is wasted work and divergent across resources.
699
+
700
+ **Fix:** Pass through. The repository / adapter handles its native shape.
701
+
702
+ ---
703
+
704
+ ## §32a. Importing pagination types from primitives or mongokit 🟠
705
+
706
+ **Detection:**
707
+ ```
708
+ pattern: "from\\s+['\"]@classytic/(primitives/pagination|mongokit)['\"][^\\n]*\\b(OffsetPaginationResult|KeysetPaginationResult|AggregatePaginationResult|PaginationResult|toCanonicalList)\\b"
709
+ output_mode: content
710
+ ```
711
+ Also `@classytic/arc-next/api` for `OffsetPaginationResponse` / `KeysetPaginationResponse` / `AggregatePaginationResponse` / `PaginatedResponse`.
712
+
713
+ **Why high:** Pagination types and the `toCanonicalList()` helper now live in `@classytic/repo-core/pagination` as the single source of truth (primitives' duplicate dropped, mongokit's local declarations dropped, arc-next's response types removed). Importing from any other package risks drift between the type the host believes and the one the kit actually returns.
714
+
715
+ **Fix:**
716
+ ```typescript
717
+ import type {
718
+ OffsetPaginationResult, KeysetPaginationResult,
719
+ AggregatePaginationResult, PaginationResult,
720
+ } from '@classytic/repo-core/pagination';
721
+ import { toCanonicalList } from '@classytic/repo-core/pagination';
722
+ ```
723
+ The wire envelope carries `method` ('offset' | 'keyset' | 'aggregate') as the discriminant; arc's `reply.sendList()` calls `toCanonicalList` internally.
724
+
725
+ ---
726
+
727
+ ## §32b. Importing event types from arc 🟠
728
+
729
+ **Detection:**
730
+ ```
731
+ pattern: "from\\s+['\"]@classytic/arc/events['\"][^\\n]*\\b(EventMeta|DomainEvent|EventHandler|EventLogger|EventTransport|DeadLetteredEvent|PublishManyResult|createEvent|createChildEvent|matchEventPattern)\\b"
732
+ output_mode: content
733
+ ```
734
+
735
+ **Why high:** Event types now live in `@classytic/primitives/events` as the single source of truth. Arc only re-exports the runtime `MemoryEventTransport`. Hosts importing event types from arc lock themselves to arc's version even when the type lives in primitives.
736
+
737
+ **Fix:**
738
+ ```typescript
739
+ import type {
740
+ EventMeta, DomainEvent, EventHandler, EventLogger, EventTransport,
741
+ DeadLetteredEvent, PublishManyResult,
742
+ } from '@classytic/primitives/events';
743
+ import { createEvent, createChildEvent, matchEventPattern } from '@classytic/primitives/events';
744
+ import { MemoryEventTransport } from '@classytic/arc/events'; // runtime stays at arc
745
+ ```
746
+
747
+ ---
748
+
749
+ ## §32c. Importing tenant config from primitives 🟡
750
+
751
+ **Detection:**
752
+ ```
753
+ pattern: "from\\s+['\"]@classytic/primitives/tenant['\"]"
754
+ output_mode: content
755
+ ```
756
+
757
+ **Why medium:** `TenantConfig`, `TenantStrategy`, `TenantFieldType`, `resolveTenantConfig`, `DEFAULT_TENANT_CONFIG`, `ResolvedTenantConfig` now live in `@classytic/repo-core/tenant`. Mongokit and sqlitekit both `extends Pick<TenantConfig, ...>` from repo-core.
758
+
759
+ **Fix:**
760
+ ```typescript
761
+ import type { TenantConfig, ResolvedTenantConfig } from '@classytic/repo-core/tenant';
762
+ import { resolveTenantConfig, DEFAULT_TENANT_CONFIG } from '@classytic/repo-core/tenant';
763
+ ```
764
+
765
+ ---
766
+
767
+ ## §32d. Importing error types from primitives or mongokit 🟠
768
+
769
+ **Detection:**
770
+ ```
771
+ pattern: "from\\s+['\"]@classytic/(primitives/errors|mongokit)['\"][^\\n]*\\bHttpError\\b"
772
+ output_mode: content
773
+ ```
774
+
775
+ **Why high:** `HttpError` (throwable contract), `ErrorContract` (wire shape), `ErrorDetail`, `ErrorCode`, `ERROR_CODES`, `toErrorContract()`, `statusToErrorCode()` all live in `@classytic/repo-core/errors`. `ArcError implements HttpError` from repo-core. Mongokit no longer publishes its own `HttpError`.
776
+
777
+ **Fix:**
778
+ ```typescript
779
+ import type { HttpError, ErrorContract, ErrorDetail, ErrorCode } from '@classytic/repo-core/errors';
780
+ import { toErrorContract, statusToErrorCode, ERROR_CODES } from '@classytic/repo-core/errors';
781
+ ```
782
+
783
+ ---
784
+
785
+ ## §32e. Hand-rolled `if (!getOrgId(scope)) throw …` 🟡
786
+
787
+ **Detection:**
788
+ ```
789
+ pattern: "if\\s*\\(\\s*!\\s*getOrgId\\s*\\("
790
+ output_mode: content
791
+ ```
792
+ Also: `if (!getUserId(...))`, `if (!getClientId(...))`, `if (!getTeamId(...))` followed by a manual throw.
793
+
794
+ **Why medium:** Arc 2.12 ships `requireOrgId(scope, hint?)` / `requireUserId(scope, hint?)` / `requireClientId(scope, hint?)` / `requireTeamId(scope, hint?)` which return the value or throw a 403 `ArcError`. Hand-rolled guards usually throw a generic `Error` (wrong status), forget the optional hint, or duplicate the same boilerplate at every call site.
795
+
796
+ **Fix:**
797
+ ```typescript
798
+ import { requireOrgId, requireUserId } from '@classytic/arc/scope';
799
+
800
+ const orgId = requireOrgId(scope); // throws 403 if missing
801
+ const userId = requireUserId(scope, 'finalize-checkout'); // hint surfaces in error.details
802
+ ```
803
+
804
+ ---
805
+
806
+ ## §32f. `createMongooseAdapter` without `schemaGenerator` 🟠
807
+
808
+ **Detection:**
809
+ ```
810
+ pattern: "createMongooseAdapter\\s*\\(\\s*\\{[^}]*\\}\\s*\\)"
811
+ multiline: true
812
+ ```
813
+ Then check whether the object literal includes a `schemaGenerator:` key.
814
+
815
+ Also: `createDrizzleAdapter` without `schemaGenerator: buildCrudSchemasFromTable`.
816
+
817
+ **Why high:** Arc 2.12 deleted the built-in mongoose / drizzle schema-gen fallback (~290 LOC). If `schemaGenerator` is omitted, OpenAPI bodies are emitted as `null` rather than silently inferred — the route still serves traffic but the docs / MCP tool input schemas are empty.
818
+
819
+ **Fix:**
820
+ ```typescript
821
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+
822
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit';
823
+
824
+ createMongooseAdapter({
825
+ model: ProductModel,
826
+ repository: productRepo,
827
+ schemaGenerator: buildCrudSchemasFromModel, // required
828
+ });
829
+ ```
830
+ For drizzle: `import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter'` + `schemaGenerator: buildCrudSchemasFromTable` from `@classytic/sqlitekit`.
831
+
832
+ ---
833
+
834
+ ## §32g. Importing kit-specific adapters from `@classytic/arc` (any subpath) 🔴
835
+
836
+ **Detection:**
837
+ ```
838
+ pattern: "from\\s+['\"]@classytic/arc(/adapters)?['\"][^\\n]*\\b(createMongooseAdapter|MongooseAdapter|createDrizzleAdapter|DrizzleAdapter|createPrismaAdapter|PrismaAdapter|PrismaQueryParser|InferMongooseDoc|MongooseDocument|isMongooseModel|MongooseAdapterOptions|DrizzleAdapterOptions|DrizzleColumnLike|DrizzleTableLike)\\b"
839
+ output_mode: content
840
+ ```
841
+
842
+ Also: adapter contract types imported from arc — `import type { DataAdapter, AdapterRepositoryInput, AdapterFactory, OpenApiSchemas, SchemaMetadata, FieldMetadata, RelationMetadata, AdapterValidationResult, AdapterSchemaContext } from '@classytic/arc'` (any subpath). And: `mergeFieldRuleConstraints` from `@classytic/arc/adapters`. Any `from '@classytic/arc/adapters'` import — the entire subpath was removed in arc 2.12.
843
+
844
+ **Why critical:** This only worked in arc ≤ 2.x. Arc 2.12 moved every kit-specific adapter (Mongoose, Drizzle, Prisma) into its kit and the cross-framework adapter contract into `@classytic/repo-core/adapter`. The `@classytic/arc/adapters` subpath was removed entirely. Importing these names from arc fails to resolve on 3.x — the build breaks at install time. The new shape is **strict**: kit-specific things MUST come from the kit; the contract MUST come from repo-core.
845
+
846
+ **Fix:**
847
+ ```typescript
848
+ // Mongoose
849
+ import {
850
+ createMongooseAdapter, MongooseAdapter,
851
+ type MongooseAdapterOptions, type InferMongooseDoc,
852
+ type MongooseDocument, isMongooseModel,
853
+ } from '@classytic/mongokit/adapter';
854
+
855
+ // Drizzle
856
+ import {
857
+ createDrizzleAdapter, DrizzleAdapter,
858
+ type DrizzleAdapterOptions, type DrizzleColumnLike, type DrizzleTableLike,
859
+ } from '@classytic/sqlitekit/adapter';
860
+
861
+ // Prisma
862
+ import {
863
+ createPrismaAdapter, PrismaAdapter, PrismaQueryParser,
864
+ } from '@classytic/prismakit/adapter';
865
+
866
+ // Cross-framework adapter contract
867
+ import type {
868
+ DataAdapter, RepositoryLike, AdapterRepositoryInput,
869
+ AdapterFactory, AdapterValidationResult, AdapterSchemaContext,
870
+ OpenApiSchemas, SchemaMetadata, FieldMetadata, RelationMetadata,
871
+ } from '@classytic/repo-core/adapter';
872
+ import { asRepositoryLike, isRepository } from '@classytic/repo-core/adapter';
873
+
874
+ // Schema helpers
875
+ import { mergeFieldRuleConstraints, applyNullable } from '@classytic/repo-core/schema';
876
+ ```
877
+
878
+ The `@classytic/arc/adapters` subpath has been **removed** in arc 2.12 — no symbols ship there anymore. `RepositoryLike` is re-exported from `@classytic/arc` for convenience, but importing from `@classytic/repo-core/adapter` is canonical. Custom kits implementing `DataAdapter<TDoc>` from `@classytic/repo-core/adapter` plug in identically.
879
+
880
+ ---
881
+
882
+ ## §32. Headers set in `onSend` hook 🔴
883
+
884
+ **Detection:**
885
+ ```
886
+ pattern: "addHook\\(['\"]onSend"
887
+ output_mode: content
888
+ ```
889
+ Then check whether the hook body sets headers (`reply.header(`, `reply.headers[`).
890
+
891
+ **Why critical:** Async `onSend` races with Fastify's `onSendEnd → safeWriteHead` flush path and produces `ERR_HTTP_HEADERS_SENT` under slow responses. Intermittent prod failures, hard to reproduce.
892
+
893
+ **Fix:** Set headers in `onRequest` (before handler) or `preSerialization` (before flush). Never `onSend`.
894
+
895
+ ---
896
+
897
+ ## Detection checklist (run-order)
898
+
899
+ Run sweeps in this order — early hits often invalidate later context:
900
+
901
+ 1. §10 — driver imports outside adapters (most architecturally significant)
902
+ 2. §28 — mongoose without mongokit (scope of mongokit migration)
903
+ 3. §3 — manual CRUD route count (scope of defineResource migration)
904
+ 4. §4 + §9 + §17 — auth/scope (security-critical)
905
+ 5. §5 — toJSON / response stripping (security-critical)
906
+ 6. §25, §27, §32 — webhook/SSE/header gotchas (security-critical)
907
+ 7. §1, §2, §6, §16 — query/schema/openapi/search (style + drift)
908
+ 8. §7, §8, §22, §23 — events/cache/idempotency/audit (silent inconsistency)
909
+ 9. §14, §18, §19 — preset adoption + custom controller wiring
910
+ 10. §11, §12, §13, §29, §30, §31 — style and edges
911
+ 11. §32a–§32g — canonical-contract drift (pagination / events / tenant / errors / adapter imports), missing `requireOrgId` accessors, missing `schemaGenerator`, kit-specific adapter imports from arc (3.0 break)