@classytic/arc 2.11.3 → 2.13.1

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 (185) hide show
  1. package/README.md +27 -18
  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/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/audit/index.mjs +1 -1
  9. package/dist/auth/audit.d.mts +199 -0
  10. package/dist/auth/audit.mjs +288 -0
  11. package/dist/auth/index.d.mts +5 -5
  12. package/dist/auth/index.mjs +117 -191
  13. package/dist/auth/redis-session.d.mts +1 -1
  14. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  15. package/dist/buildHandler-olo-gt94.mjs +610 -0
  16. package/dist/cache/index.d.mts +3 -3
  17. package/dist/cache/index.mjs +3 -3
  18. package/dist/cli/commands/describe.d.mts +89 -13
  19. package/dist/cli/commands/describe.mjs +56 -2
  20. package/dist/cli/commands/docs.mjs +2 -2
  21. package/dist/cli/commands/generate.mjs +147 -48
  22. package/dist/cli/commands/init.d.mts +13 -0
  23. package/dist/cli/commands/init.mjs +237 -112
  24. package/dist/cli/commands/introspect.mjs +8 -1
  25. package/dist/context/index.mjs +1 -1
  26. package/dist/core/index.d.mts +3 -3
  27. package/dist/core/index.mjs +5 -5
  28. package/dist/core-D72ia0EH.mjs +1399 -0
  29. package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
  30. package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
  31. package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
  32. package/dist/defineEvent-D5h7EvAx.mjs +188 -0
  33. package/dist/docs/index.d.mts +2 -2
  34. package/dist/docs/index.mjs +2 -2
  35. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  36. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  37. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  38. package/dist/errors-j4aJm1Wg.mjs +184 -0
  39. package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
  40. package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
  41. package/dist/events/index.d.mts +164 -5
  42. package/dist/events/index.mjs +133 -209
  43. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  44. package/dist/events/transports/redis-stream-entry.mjs +204 -31
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +2 -2
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
  49. package/dist/hooks/index.d.mts +1 -1
  50. package/dist/hooks/index.mjs +1 -1
  51. package/dist/idempotency/index.d.mts +3 -3
  52. package/dist/idempotency/index.mjs +1 -20
  53. package/dist/idempotency/redis.d.mts +1 -1
  54. package/dist/idempotency/redis.mjs +1 -1
  55. package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
  56. package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
  57. package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
  58. package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  59. package/dist/index.d.mts +6 -7
  60. package/dist/index.mjs +9 -10
  61. package/dist/integrations/event-gateway.d.mts +2 -2
  62. package/dist/integrations/event-gateway.mjs +1 -1
  63. package/dist/integrations/index.d.mts +2 -2
  64. package/dist/integrations/mcp/index.d.mts +2 -2
  65. package/dist/integrations/mcp/index.mjs +1 -1
  66. package/dist/integrations/mcp/testing.d.mts +1 -1
  67. package/dist/integrations/mcp/testing.mjs +1 -1
  68. package/dist/integrations/streamline.d.mts +60 -11
  69. package/dist/integrations/streamline.mjs +75 -85
  70. package/dist/integrations/websocket-redis.d.mts +1 -1
  71. package/dist/integrations/websocket.d.mts +1 -1
  72. package/dist/integrations/websocket.mjs +2 -8
  73. package/dist/middleware/index.d.mts +1 -1
  74. package/dist/middleware/index.mjs +2 -2
  75. package/dist/migrations/index.d.mts +23 -3
  76. package/dist/migrations/index.mjs +0 -7
  77. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  78. package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
  79. package/dist/org/index.d.mts +2 -2
  80. package/dist/org/index.mjs +1 -1
  81. package/dist/permissions/index.d.mts +3 -3
  82. package/dist/permissions/index.mjs +3 -3
  83. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  84. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  85. package/dist/pipeline/index.d.mts +1 -1
  86. package/dist/pipeline/index.mjs +1 -1
  87. package/dist/plugins/index.d.mts +18 -33
  88. package/dist/plugins/index.mjs +33 -13
  89. package/dist/plugins/response-cache.mjs +1 -1
  90. package/dist/plugins/tracing-entry.d.mts +1 -1
  91. package/dist/plugins/tracing-entry.mjs +1 -1
  92. package/dist/presets/filesUpload.d.mts +5 -5
  93. package/dist/presets/filesUpload.mjs +6 -9
  94. package/dist/presets/index.d.mts +1 -1
  95. package/dist/presets/index.mjs +1 -1
  96. package/dist/presets/multiTenant.d.mts +1 -1
  97. package/dist/presets/multiTenant.mjs +2 -2
  98. package/dist/presets/search.d.mts +2 -2
  99. package/dist/presets/search.mjs +6 -8
  100. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  101. package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
  102. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  103. package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
  104. package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
  105. package/dist/registry/index.d.mts +1 -1
  106. package/dist/registry/index.mjs +2 -2
  107. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  108. package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
  109. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
  110. package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +100 -30
  112. package/dist/schemas/index.mjs +86 -29
  113. package/dist/scim/index.d.mts +264 -0
  114. package/dist/scim/index.mjs +963 -0
  115. package/dist/scope/index.d.mts +3 -3
  116. package/dist/scope/index.mjs +4 -4
  117. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  118. package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  119. package/dist/testing/index.d.mts +2 -8
  120. package/dist/testing/index.mjs +16 -24
  121. package/dist/testing/storageContract.d.mts +1 -1
  122. package/dist/types/index.d.mts +4 -4
  123. package/dist/types/storage.d.mts +1 -1
  124. package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
  125. package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
  126. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  127. package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
  128. package/dist/utils/index.d.mts +2 -2
  129. package/dist/utils/index.mjs +5 -5
  130. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  131. package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  132. package/package.json +24 -34
  133. package/skills/arc/SKILL.md +521 -785
  134. package/skills/arc/references/agent-auth.md +238 -0
  135. package/skills/arc/references/api-reference.md +187 -0
  136. package/skills/arc/references/auth.md +354 -7
  137. package/skills/arc/references/enterprise-auth.md +94 -0
  138. package/skills/arc/references/events.md +8 -6
  139. package/skills/arc/references/mcp.md +2 -2
  140. package/skills/arc/references/multi-tenancy.md +11 -2
  141. package/skills/arc/references/production.md +10 -9
  142. package/skills/arc/references/scim.md +247 -0
  143. package/skills/arc/references/testing.md +1 -1
  144. package/skills/arc-code-review/SKILL.md +141 -0
  145. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  146. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  147. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  148. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  149. package/skills/arc-code-review/references/scaffolding.md +230 -0
  150. package/skills/arc-code-review/references/severity.md +127 -0
  151. package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
  152. package/dist/adapters/index.d.mts +0 -3
  153. package/dist/adapters/index.mjs +0 -2
  154. package/dist/adapters-D0tT2Tyo.mjs +0 -949
  155. package/dist/auth/mongoose.d.mts +0 -191
  156. package/dist/auth/mongoose.mjs +0 -73
  157. package/dist/core-DnUsRpuX.mjs +0 -1049
  158. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  159. package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
  160. package/dist/errors-D5c-5BJL.mjs +0 -232
  161. package/dist/index-BbMrcvGp.d.mts +0 -362
  162. package/dist/redis-stream-CM8TXTix.d.mts +0 -110
  163. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  164. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  165. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  166. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  167. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  168. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
  169. /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
  170. /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
  171. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  172. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  173. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  174. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  175. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
  176. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  177. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  178. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  179. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
  180. /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
  181. /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
  182. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  183. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  184. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
  185. /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
@@ -0,0 +1,386 @@
1
+ # Mongoose → @classytic/mongokit Migration
2
+
3
+ For projects that have arc installed but still use raw Mongoose models with hand-rolled repository classes, manual pagination, scattered `pre`/`post` hooks, and per-schema `toJSON` transforms.
4
+
5
+ Mongokit is a Mongoose enhancement layer that:
6
+ - Wraps a `mongoose.Model` in a `Repository<TDoc>` that implements `MinimalRepo` + `Partial<StandardRepo>` from `@classytic/repo-core` (so it plugs into the arc adapter with no casts).
7
+ - Ships the **arc adapter itself** at `@classytic/mongokit/adapter` (since 3.13.0 / arc 2.12). Through arc 2.12 the adapter lived in arc; in arc 2.12 every kit-specific adapter moved out (Mongoose to mongokit, Drizzle to sqlitekit, Prisma to prismakit) so arc no longer pulls a DB driver into any consumer's resolution graph.
8
+ - Provides 15+ plugins: `timestampPlugin`, `softDeletePlugin`, `multiTenantPlugin`, `validationChainPlugin`, `cachePlugin`, `auditTrailPlugin`, `auditLogPlugin`, `cascadePlugin`, `customIdPlugin`, `observabilityPlugin`, `elasticSearchPlugin`, `mongoOperationsPlugin`, `aggregateHelpersPlugin`, `fieldFilterPlugin`, `batchOperationsPlugin`.
9
+ - Ships `QueryParser`, `AggregationBuilder`, `LookupBuilder`, `PaginationEngine` (offset + keyset auto-detect).
10
+ - Provides `withTransaction` helper with auto-retry and standalone fallback.
11
+
12
+ **It does NOT**:
13
+ - Set `schema.set('toJSON', ...)` — read transforms are framework/controller responsibility (arc handles it via `fieldRules.hidden`).
14
+ - Own connection lifecycle — `mongoose.connect()` stays in your bootstrap.
15
+
16
+ ---
17
+
18
+ ## Detection — is mongokit migration needed?
19
+
20
+ Add to the audit detection sweep:
21
+
22
+ | Signal | Detection |
23
+ |---|---|
24
+ | Mongoose without mongokit | `package.json`: `mongoose` present, `@classytic/mongokit` absent |
25
+ | Per-model repo class | `Grep "class \\w+Repository\\b"` — count classes |
26
+ | Manual `pre`/`post` hooks | `Grep "schema\\.(pre\\|post)\\(['\"]"` |
27
+ | Hand-rolled `toJSON` | `Grep "schema\\.set\\(['\"]toJSON\\|toJSON\\s*=\\s*function"` |
28
+ | Manual pagination | `Grep "skip\\(.*page\\|countDocuments\\("` |
29
+ | Per-method tenant filter | `Grep "organizationId.*req\\.user\\|orgId.*scope"` (paired with `find` / `findOne`) |
30
+ | Manual `findByIdAndUpdate(..., { runValidators: true })` | `Grep "runValidators"` |
31
+ | Manual transaction session threading | `Grep "session.*startSession\\|session\\.commitTransaction"` |
32
+
33
+ If `mongoose` deps + `@classytic/mongokit` absent → every model is a candidate. Estimate: a typical 150 LOC repo class shrinks to ~30 LOC with mongokit + plugins.
34
+
35
+ ---
36
+
37
+ ## Mapping table — Mongoose pattern → mongokit replacement
38
+
39
+ | Hand-rolled Mongoose | Mongokit replacement |
40
+ |---|---|
41
+ | `class UserRepository { async create(d) { return User.create(d) } /* + 15 more */ }` | `new Repository(User)` (or extend for domain verbs) |
42
+ | `userSchema.pre('save', function(next) { this.updatedAt = new Date(); next(); })` | `timestampPlugin()` |
43
+ | `userSchema.set('toJSON', { transform: (_, ret) => { delete ret.password; ... } })` | Arc `fieldRules: { password: { hidden: true } }` (mongokit doesn't do this) |
44
+ | `Model.find({ deletedAt: null, ...filters })` everywhere | `softDeletePlugin({ deletedField: 'deletedAt' })` |
45
+ | `Model.find({ ...filters, organizationId: orgId })` everywhere | `multiTenantPlugin({ tenantField: 'organizationId', contextKey: 'organizationId' })` |
46
+ | Manual pagination wrappers (skip/limit + countDocuments) | `repo.getAll({ page, limit, sort })` (offset) or `repo.getAll({ sort, after: cursor })` (keyset auto-detect) |
47
+ | `Model.findOne({ email })` for unique check before create | `validationChainPlugin([uniqueField('email')])` |
48
+ | Manual `findOneAndUpdate(..., { new: true, runValidators: true })` | `repo.update(id, data)` (validators on by default) |
49
+ | Manual `mongoose.startSession()` + `withTransaction` boilerplate | `repo.withTransaction(async (session) => { ... })` |
50
+ | Manual cache invalidation around `Model.find` | `cachePlugin({ adapter, ttl, byIdTtl })` |
51
+ | Manual audit-log writes on every mutation | `auditTrailPlugin()` + `auditLogPlugin()` |
52
+ | Hand-coded URL → query parsing | `new QueryParser({ schema, allowedFilterFields, ... })` |
53
+ | Manual `$lookup`/`$match` aggregation building | `AggregationBuilder` / `LookupBuilder` |
54
+ | Stripe-style prefixed IDs (`cus_xxx`, `ord_xxx`) | `customIdPlugin({ strategy, publicIdField })` |
55
+
56
+ Hooks fire with priority ordering: `POLICY` (100, multi-tenant + soft-delete pre-filtering) → `CACHE` (200) → `OBSERVABILITY` (300) → `DEFAULT` (500, user hooks).
57
+
58
+ ---
59
+
60
+ ## Recipe — User model migration
61
+
62
+ ### Before — Mongoose only (~140 LOC)
63
+
64
+ ```typescript
65
+ // models/user.ts
66
+ import { Schema, model, Document } from 'mongoose';
67
+
68
+ export interface UserDoc extends Document {
69
+ name: string;
70
+ email: string;
71
+ password: string;
72
+ organizationId: Types.ObjectId;
73
+ deletedAt: Date | null;
74
+ createdAt: Date;
75
+ updatedAt: Date;
76
+ }
77
+
78
+ const userSchema = new Schema<UserDoc>({
79
+ name: { type: String, required: true },
80
+ email: { type: String, required: true, unique: true },
81
+ password: { type: String, required: true },
82
+ organizationId: { type: Schema.Types.ObjectId, required: true, ref: 'Organization' },
83
+ deletedAt: { type: Date, default: null },
84
+ createdAt: { type: Date },
85
+ updatedAt: { type: Date },
86
+ });
87
+
88
+ userSchema.pre('save', function (next) {
89
+ if (this.isNew) this.createdAt = new Date();
90
+ this.updatedAt = new Date();
91
+ next();
92
+ });
93
+
94
+ userSchema.set('toJSON', {
95
+ transform: (_doc, ret) => {
96
+ delete ret.password;
97
+ delete ret.__v;
98
+ return ret;
99
+ },
100
+ });
101
+
102
+ export const User = model<UserDoc>('User', userSchema);
103
+ ```
104
+
105
+ ```typescript
106
+ // repositories/userRepository.ts
107
+ import { User, UserDoc } from '../models/user.js';
108
+
109
+ export class UserRepository {
110
+ async create(data: Partial<UserDoc>, orgId: string) {
111
+ const exists = await User.findOne({ email: data.email });
112
+ if (exists) throw new Error('Email taken');
113
+ return User.create({ ...data, organizationId: orgId });
114
+ }
115
+
116
+ async getById(id: string, orgId: string) {
117
+ return User.findOne({ _id: id, organizationId: orgId, deletedAt: null }).lean();
118
+ }
119
+
120
+ async list(orgId: string, page: number, limit: number, filters: Record<string, unknown>) {
121
+ const query = { ...filters, organizationId: orgId, deletedAt: null };
122
+ const skip = (page - 1) * limit;
123
+ const docs = await User.find(query).skip(skip).limit(limit).sort({ createdAt: -1 }).lean();
124
+ const total = await User.countDocuments(query);
125
+ return { docs, total, page, pages: Math.ceil(total / limit) };
126
+ }
127
+
128
+ async update(id: string, data: Partial<UserDoc>, orgId: string) {
129
+ return User.findOneAndUpdate(
130
+ { _id: id, organizationId: orgId, deletedAt: null },
131
+ { ...data, updatedAt: new Date() },
132
+ { new: true, runValidators: true },
133
+ ).lean();
134
+ }
135
+
136
+ async softDelete(id: string, orgId: string) {
137
+ return User.findOneAndUpdate(
138
+ { _id: id, organizationId: orgId, deletedAt: null },
139
+ { deletedAt: new Date() },
140
+ { new: true },
141
+ ).lean();
142
+ }
143
+
144
+ async restore(id: string, orgId: string) {
145
+ return User.findOneAndUpdate(
146
+ { _id: id, organizationId: orgId, deletedAt: { $ne: null } },
147
+ { deletedAt: null },
148
+ { new: true },
149
+ ).lean();
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### After — Mongoose + mongokit (~50 LOC)
155
+
156
+ ```typescript
157
+ // models/user.ts (unchanged structure — drop pre('save') and toJSON transform)
158
+ import { Schema, model, Document } from 'mongoose';
159
+
160
+ export interface UserDoc extends Document {
161
+ name: string;
162
+ email: string;
163
+ password: string;
164
+ organizationId: Types.ObjectId;
165
+ deletedAt: Date | null;
166
+ createdAt: Date;
167
+ updatedAt: Date;
168
+ }
169
+
170
+ const userSchema = new Schema<UserDoc>({
171
+ name: { type: String, required: true },
172
+ email: { type: String, required: true, unique: true },
173
+ password: { type: String, required: true },
174
+ organizationId: { type: Schema.Types.ObjectId, required: true, ref: 'Organization' },
175
+ deletedAt: { type: Date, default: null },
176
+ createdAt: { type: Date },
177
+ updatedAt: { type: Date },
178
+ });
179
+
180
+ export const User = model<UserDoc>('User', userSchema);
181
+ ```
182
+
183
+ ```typescript
184
+ // repositories/userRepository.ts
185
+ import { Repository, methodRegistryPlugin, timestampPlugin, softDeletePlugin,
186
+ multiTenantPlugin, validationChainPlugin } from '@classytic/mongokit';
187
+ import { uniqueField } from '@classytic/mongokit/plugins/validators';
188
+ import { User, UserDoc } from '../models/user.js';
189
+
190
+ export const userRepo = new Repository<UserDoc>(User, [
191
+ methodRegistryPlugin(),
192
+ timestampPlugin(),
193
+ multiTenantPlugin({ tenantField: 'organizationId', contextKey: 'organizationId', required: true }),
194
+ softDeletePlugin({ deletedField: 'deletedAt' }),
195
+ validationChainPlugin([uniqueField('email', 'Email already in use')]),
196
+ ]);
197
+ ```
198
+
199
+ ```typescript
200
+ // resources/user/user.resource.ts — arc resource consuming the mongokit repo
201
+ import { defineResource, requireRoles, allowPublic, fields } from '@classytic/arc';
202
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+: from the kit
203
+ import { buildCrudSchemasFromModel } from '@classytic/mongokit';
204
+ import { User } from '../../models/user.js';
205
+ import { userRepo } from '../../repositories/userRepository.js';
206
+
207
+ export const userResource = defineResource({
208
+ name: 'user',
209
+ adapter: createMongooseAdapter({ model: User, repository: userRepo, schemaGenerator: buildCrudSchemasFromModel }),
210
+ presets: ['softDelete'], // multiTenant is at the repo layer (mongokit), so we don't need the arc preset too — pick one
211
+ permissions: {
212
+ list: requireRoles(['admin']),
213
+ get: allowPublic(),
214
+ create: requireRoles(['admin']),
215
+ update: requireRoles(['admin']),
216
+ delete: requireRoles(['admin']),
217
+ },
218
+ schemaOptions: {
219
+ fieldRules: {
220
+ password: fields.hidden(),
221
+ organizationId: { systemManaged: true },
222
+ },
223
+ },
224
+ });
225
+ ```
226
+
227
+ **What changed:**
228
+ - Repository class: 80 LOC → 8 LOC.
229
+ - `pre('save')` for timestamps → `timestampPlugin()` (1 line).
230
+ - Tenant scoping: 5 duplicated places → 1 plugin.
231
+ - Soft-delete with `restore`/`getDeleted`: ~30 LOC → 1 plugin.
232
+ - Email uniqueness: hand-coded `findOne` race → `validationChainPlugin([uniqueField(...)])`.
233
+ - `toJSON` transform → arc `fields.hidden()` (works on `.lean()` too).
234
+ - `findByIdAndUpdate(..., { runValidators: true })` boilerplate → `repo.update(id, data)`.
235
+
236
+ ---
237
+
238
+ ## Where to put multi-tenant scoping — repo or resource?
239
+
240
+ **Decision rule:** put it in **one** layer, not both. Two recommended patterns:
241
+
242
+ **Option A — at the repo layer (mongokit plugin):**
243
+ ```typescript
244
+ new Repository(User, [multiTenantPlugin({ tenantField: 'organizationId', contextKey: 'organizationId' })]);
245
+ ```
246
+ - Pros: works for non-HTTP callers (jobs, CLI, hooks). Centralized.
247
+ - Cons: requires passing a `RepositoryContext` with `organizationId` to every call.
248
+
249
+ **Option B — at the arc resource layer (preset):**
250
+ ```typescript
251
+ defineResource({ presets: [{ name: 'multiTenant', tenantField: 'organizationId' }] });
252
+ ```
253
+ - Pros: arc handles context threading from `request.scope` automatically.
254
+ - Cons: scoped only to HTTP/MCP entry points; jobs/CLI need to thread context themselves.
255
+
256
+ **For new code:** prefer (B) — arc is the boundary. If you have non-HTTP entrypoints that hit the same repo, layer (A) on top of (B) and let the repo plugin be a defense-in-depth check (it'll no-op when arc has already injected the filter).
257
+
258
+ ---
259
+
260
+ ## Hook system parity
261
+
262
+ Mongoose `schema.pre('save', ...)` is per-model and per-method. Mongokit hooks are per-repo and per-event with priority ordering:
263
+
264
+ ```typescript
265
+ repo.on('before:create', async (ctx) => { /* validate, mutate ctx.data */ });
266
+ repo.on('after:create', async (event) => { /* event = { context, result } */ });
267
+ repo.on('error:create', async (event) => { /* event = { context, error } */ });
268
+ ```
269
+
270
+ **Events:** `before:{create|update|delete|findAll|getById|...}`, `after:*`, `error:*`.
271
+ **Priorities:** `POLICY = 100` (filter injection runs first) → `CACHE = 200` → `OBSERVABILITY = 300` → `DEFAULT = 500`.
272
+
273
+ User hooks default to `DEFAULT` (500). Override with `repo.on('before:create', fn, { priority: HOOK_PRIORITY.POLICY })`.
274
+
275
+ Migrating Mongoose hooks:
276
+ - `schema.pre('save', ...)` (insert + update both) → split into `before:create` and `before:update`.
277
+ - `schema.pre('findOneAndUpdate', ...)` → `before:update`.
278
+ - `schema.pre('remove', ...)` → `before:delete`.
279
+ - `schema.post('save', ...)` → `after:create` / `after:update`.
280
+
281
+ ---
282
+
283
+ ## Pagination
284
+
285
+ Auto-detected by `getAll`:
286
+ ```typescript
287
+ // Offset (page-based)
288
+ await repo.getAll({ page: 2, limit: 20, sort: { createdAt: -1 } });
289
+ // → { method: 'offset', docs, total, pages, hasNext, hasPrev }
290
+
291
+ // Keyset (cursor)
292
+ const p1 = await repo.getAll({ sort: { createdAt: -1 }, limit: 20 });
293
+ const p2 = await repo.getAll({ sort: { createdAt: -1 }, limit: 20, after: p1.next });
294
+ // → { method: 'keyset', docs, hasMore, next }
295
+
296
+ // Aggregate paginate (with $near rewriting handled)
297
+ await repo.aggregatePipelinePaginate(pipeline, { page, limit });
298
+ ```
299
+
300
+ Drop hand-rolled `Math.ceil(total / limit)` and pagination DTOs — mongokit returns the envelope.
301
+
302
+ ---
303
+
304
+ ## Transactions
305
+
306
+ ```typescript
307
+ await repo.withTransaction(async (session) => {
308
+ await repo.update(id, { status: 'paid' }, { session });
309
+ await invoiceRepo.create(invoiceData, { session });
310
+ });
311
+ // Auto-retries on TransientTransactionError / UnknownTransactionCommitResult
312
+ // Falls back to non-transactional when running on standalone Mongo (with allowFallback: true)
313
+ ```
314
+
315
+ Cross-repo transaction:
316
+ ```typescript
317
+ import { withTransaction } from '@classytic/mongokit';
318
+ await withTransaction(mongoose.connection, async (session) => {
319
+ await orderRepo.update(id, ..., { session });
320
+ await invoiceRepo.create(..., { session });
321
+ });
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Per-resource adoption order
327
+
328
+ For each Mongoose model in the project:
329
+
330
+ 1. Drop `pre('save')` for timestamps; add `timestampPlugin()` to the repo.
331
+ 2. Drop `schema.set('toJSON', ...)`; declare sensitive fields in arc `fieldRules: { password: fields.hidden() }`.
332
+ 3. Replace the per-model repository class with `new Repository(Model, [...plugins])`.
333
+ 4. Convert Mongoose hooks (`schema.pre/post`) to `repo.on('before:*'/'after:*', ...)`.
334
+ 5. If multi-tenant: pick repo-layer or resource-layer scoping (one, not both).
335
+ 6. If soft-delete: add `softDeletePlugin` (drops the manual `deletedAt: null` filter scattered across reads).
336
+ 7. Replace pagination wrappers with `repo.getAll(...)`.
337
+ 8. Wire the repo into the arc resource via `createMongooseAdapter({ model, repository: repo, schemaGenerator: buildCrudSchemasFromModel })`.
338
+ 9. Run the existing test suite — should pass with no logic changes (mongokit's defaults match Mongoose semantics).
339
+ 10. Add `npm run typecheck:tests` step in CI to lock conformance with `@classytic/repo-core`'s `StandardRepo`.
340
+
341
+ ---
342
+
343
+ ## Conformance gate
344
+
345
+ If consumers see `as unknown as RepositoryLike<T>` casts when wiring a repo into the mongoose adapter, that's drift. Mongokit's `tests/unit/standard-repo-assignment.test-d.ts` proves whole-interface assignment via TypeScript. Run `npm run typecheck:tests` in mongokit; if it fails, the published version drifted from the contract — pin to a known-good version or open a PR.
346
+
347
+ In the client project, prefer `createMongooseAdapter({ model, repository, schemaGenerator })` from `@classytic/mongokit/adapter` over manual `RepositoryLike` shaping — it accepts mongokit-native repos with no casts.
348
+
349
+ ## arc 2.12 / mongokit 3.13.0 — adapter split
350
+
351
+ Through mongokit 3.12 / arc 2.11, `createMongooseAdapter` shipped from `@classytic/arc`. In mongokit 3.13.0 + arc 2.12, the adapter lives in mongokit at `@classytic/mongokit/adapter`. Coordinated minimums:
352
+
353
+ | Package | Min |
354
+ |---|---|
355
+ | `@classytic/arc` | 2.12.0 |
356
+ | `@classytic/mongokit` | 3.13.0 |
357
+ | `@classytic/repo-core` | 0.4.0 |
358
+
359
+ Migration shape:
360
+
361
+ ```typescript
362
+ // arc 2.x
363
+ import { createMongooseAdapter } from '@classytic/arc';
364
+ import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/arc';
365
+ import type { InferMongooseDoc, MongooseAdapterOptions } from '@classytic/arc/adapters';
366
+
367
+ // arc 2.12+
368
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter';
369
+ import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/repo-core/adapter';
370
+ import type { InferMongooseDoc, MongooseAdapterOptions } from '@classytic/mongokit/adapter';
371
+ ```
372
+
373
+ The kit owns mongoose as its peer dep; arc dropped both `@classytic/mongokit` and `mongoose` from its `peerDependencies`. Hosts depend on `@classytic/mongokit` directly, which transitively pulls mongoose. Detection regex for stragglers: see `references/anti-patterns.md` §32g.
374
+
375
+ ---
376
+
377
+ ## What mongokit does NOT do
378
+
379
+ Be explicit so the audit doesn't recommend it for the wrong job:
380
+
381
+ - **Connection management.** `mongoose.connect()` stays in your bootstrap.
382
+ - **Response transforms.** No `_id` → `id` rewriting, no field stripping. Use arc `fieldRules.hidden` or controller-level mapping.
383
+ - **Type generation.** No runtime codegen. Declare `interface UserDoc extends Document` manually (or use Mongoose's `InferSchemaType`).
384
+ - **Versioning.** Use `mongoOperationsPlugin` + `$inc` on `__v`, or external `mongoose-version`.
385
+ - **CLI scaffolding.** Mongokit ships no CLI — use `arc generate resource <name>` which creates a mongokit-style repo.
386
+ - **Vector/Atlas Search.** Mentioned in roadmap; not shipped in 3.12.x. Use `elasticSearchPlugin` for ES, or wire your own.
@@ -0,0 +1,230 @@
1
+ # Arc CLI & Scaffolding — what `arc init` and `arc generate` produce
2
+
3
+ When auditing a project, look for divergence between what `arc generate resource` would have created and what the team actually built. Hand-created files often:
4
+ - Skip the `*.repository.ts` layer entirely (Mongoose calls inline in routes).
5
+ - Misname files (`product-routes.ts`, `productController.ts` instead of `product.resource.ts`).
6
+ - Co-mingle resources in one file, killing tree-shake and `loadResources()` discovery.
7
+
8
+ ---
9
+
10
+ ## `arc init [name] [options]`
11
+
12
+ ```bash
13
+ arc init my-api --mongokit --jwt --ts
14
+ arc init billing --custom --better-auth --multi
15
+ arc init edge-svc --mongokit --jwt --ts --edge
16
+ ```
17
+
18
+ | Flag | Meaning | Default if omitted |
19
+ |---|---|---|
20
+ | `--mongokit` | MongoDB + mongokit repository | (required choice) |
21
+ | `--custom` | Custom adapter — template points at the `RepositoryLike` contract from `@classytic/repo-core/adapter`; pick any kit (mongokit, sqlitekit, prismakit, custom) | (required choice) |
22
+ | `--jwt` | Arc JWT auth strategy | (required choice) |
23
+ | `--better-auth` | Better Auth integration | (required choice) |
24
+ | `--single` | Single-tenant template | `--single` |
25
+ | `--multi` | Multi-tenant template (org context) | — |
26
+ | `--ts` | TypeScript | `--ts` |
27
+ | `--js` | JavaScript | — |
28
+ | `--edge` | Serverless/edge runtime optimizations | — |
29
+ | `--skip-install` | Don't run `npm install` | — |
30
+ | `--force` | Overwrite existing dir | — |
31
+
32
+ **Output structure:**
33
+ ```
34
+ my-api/
35
+ ├── .arcrc # project config for arc generate
36
+ ├── package.json # full deps + devDeps wired (npm install just works)
37
+ ├── tsconfig.json
38
+ ├── biome.json
39
+ ├── .gitignore
40
+ ├── .env.example
41
+ └── src/
42
+ ├── app.ts # createApp() entry
43
+ ├── server.ts # listen()
44
+ ├── resources/ # empty, populated by `arc generate resource`
45
+ └── models/ # Mongoose schemas (--mongokit only)
46
+ ```
47
+
48
+ The scaffold seeds full `dependencies` + `devDependencies` so `npm install` works without the CLI's pre-pass. Audit signal: a project missing `.arcrc` but using arc was likely *not* scaffolded — every file was hand-created and may diverge.
49
+
50
+ ---
51
+
52
+ ## `arc generate resource [name] [options]`
53
+
54
+ ```bash
55
+ arc generate resource product
56
+ arc generate resource product --mcp --soft-delete
57
+ arc generate resource org-profile --bulk --tree
58
+ ```
59
+
60
+ Options:
61
+ - `--mcp` — emit `{name}.mcp.ts` for custom MCP tools alongside the resource
62
+ - `--tree` — wire `tree` preset (parent-child)
63
+ - `--soft-delete` — wire `softDelete` preset
64
+ - `--bulk` — wire `bulk` preset
65
+
66
+ **Generated files (mongokit template):**
67
+ ```
68
+ src/resources/product/
69
+ ├── product.model.ts # Mongoose schema (with required imports)
70
+ ├── product.repository.ts # mongokit Repository or extends Repository<ProductDoc>
71
+ ├── product.resource.ts # defineResource() with adapter wired
72
+ └── product.mcp.ts # (--mcp only) extraTools / bridge stubs
73
+ ```
74
+
75
+ **Naming convention:**
76
+ - Input: kebab-case (`org-profile`).
77
+ - File names: `org-profile.model.ts`, `org-profile.repository.ts`, `org-profile.resource.ts`, `org-profile.mcp.ts`.
78
+ - Class names: PascalCase (`OrgProfile`, `OrgProfileRepository`).
79
+ - Variable names: camelCase (`orgProfile`, `orgProfileRepo`, `orgProfileResource`).
80
+
81
+ **Audit signal:** files like `productRoutes.ts`, `product-controller.ts`, `productHandlers.js`, `models/Product.js` (no resource file) → not scaffolded; team is fighting arc's conventions.
82
+
83
+ ---
84
+
85
+ ## `.arcrc` — project config
86
+
87
+ ```json
88
+ {
89
+ "adapter": "mongokit",
90
+ "auth": "jwt",
91
+ "tenancy": "multi",
92
+ "language": "ts",
93
+ "mcp": true,
94
+ "templates": {
95
+ "resourceDir": "src/resources",
96
+ "modelDir": "src/models"
97
+ }
98
+ }
99
+ ```
100
+
101
+ Fields:
102
+ - `adapter`: `'mongokit' | 'custom'` — controls which template `arc generate resource` uses.
103
+ - `auth`: `'jwt' | 'better-auth'` — auth example wiring in templates.
104
+ - `tenancy`: `'single' | 'multi'` — multi-tenant adds `multiTenant` preset and scope wiring by default.
105
+ - `language`: `'ts' | 'js'`.
106
+ - `mcp`: `true | false` — when true, `arc generate resource` always emits `.mcp.ts` (equivalent to `--mcp`).
107
+ - `templates.resourceDir` / `modelDir` — override default locations.
108
+
109
+ Audit: a stale `.arcrc` (e.g., points at deleted dirs) is a sign the team has drifted from CLI scaffolding. Either update or remove.
110
+
111
+ ---
112
+
113
+ ## Other CLI commands
114
+
115
+ ```bash
116
+ arc docs ./openapi.json --entry ./dist/index.js
117
+ ```
118
+ Boots the app in introspect mode and emits OpenAPI 3.x to the output path. Wire into `prebuild` or a CI artifact step. Use this in audits to diff against any hand-maintained `swagger.yaml` — discrepancies are bugs in either side.
119
+
120
+ ```bash
121
+ arc introspect --entry ./dist/index.js
122
+ ```
123
+ Lists every registered resource: name, route count, presets, permissions summary. Good first command when auditing an unfamiliar codebase.
124
+
125
+ ```bash
126
+ arc describe product --entry ./dist/index.js
127
+ ```
128
+ Detail one resource: routes, actions, permissions, field rules, presets, cache config, events. Shows what arc *thinks* the resource is — compare against hand-rolled equivalents.
129
+
130
+ ```bash
131
+ arc doctor
132
+ ```
133
+ Diagnose env (Node version, TS version, peer dep versions). Run before audit to confirm environment is sane.
134
+
135
+ ---
136
+
137
+ ## Resource file structure (canonical)
138
+
139
+ `src/resources/{name}/{name}.resource.ts`:
140
+
141
+ ```typescript
142
+ import { defineResource, requireRoles, allowPublic, fields } from '@classytic/arc';
143
+ import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+
144
+ import { Repository, buildCrudSchemasFromModel } from '@classytic/mongokit';
145
+ import { Product } from './product.model.js';
146
+
147
+ const productRepo = new Repository(Product);
148
+
149
+ export const productResource = defineResource({
150
+ name: 'product',
151
+ displayName: 'Products',
152
+ module: 'catalog',
153
+ adapter: createMongooseAdapter({ model: Product, repository: productRepo, schemaGenerator: buildCrudSchemasFromModel }),
154
+ presets: ['softDelete'],
155
+ permissions: {
156
+ list: allowPublic(),
157
+ get: allowPublic(),
158
+ create: requireRoles(['admin']),
159
+ update: requireRoles(['admin']),
160
+ delete: requireRoles(['admin']),
161
+ },
162
+ schemaOptions: {
163
+ fieldRules: {
164
+ name: { type: 'string', minLength: 1, required: true },
165
+ price: { type: 'number', minimum: 0, required: true },
166
+ slug: { type: 'string', readonly: true },
167
+ deletedAt: { type: 'date', nullable: true, hidden: true },
168
+ },
169
+ },
170
+ cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
171
+ });
172
+
173
+ export default productResource;
174
+ ```
175
+
176
+ `loadResources(import.meta.url)` discovers:
177
+ - `default` export (`export default productResource`)
178
+ - `export const resource`
179
+ - Any named export with `.toPlugin()`
180
+
181
+ Works with relative imports + Node `#` subpath imports — **NOT** tsconfig path aliases (`@/*`). See [anti-patterns.md §30](anti-patterns.md).
182
+
183
+ ---
184
+
185
+ ## Audit checks against scaffolding output
186
+
187
+ When you walk a client repo, compare against this baseline:
188
+
189
+ | Expected | Found | Implication |
190
+ |---|---|---|
191
+ | `.arcrc` at project root | missing | Team didn't scaffold — likely diverged conventions |
192
+ | `src/resources/{name}/{name}.resource.ts` | `src/routes/{name}.ts` | Hand-rolled — flag for §3 (manual CRUD) |
193
+ | `src/resources/{name}/{name}.repository.ts` extends `Repository` | `class FooRepository` with `Model.find` directly | Mongokit not adopted — flag for §28 |
194
+ | `src/app.ts` calls `createApp({ resources: [...] })` | manual `app.register(productPlugin)` per resource | Not using arc factory — possible boot-order issues |
195
+ | `src/resources/{name}/{name}.mcp.ts` (if `.arcrc` has `mcp: true`) | missing | MCP feature mentioned but not wired |
196
+ | `package.json` script `"docs:openapi": "arc docs ..."` | hand-maintained `openapi.yaml` | Out-of-band spec — flag for §6 |
197
+ | `npm run smoke` (CLI + subpath imports) | missing | No release gate — recommend wiring |
198
+
199
+ ---
200
+
201
+ ## Smoke commands worth running during audit
202
+
203
+ ```bash
204
+ # Confirm arc + mongokit + sqlitekit + repo-core versions
205
+ npm ls @classytic/arc @classytic/mongokit @classytic/sqlitekit @classytic/repo-core
206
+
207
+ # List all defineResource calls
208
+ grep -rn "defineResource(" src/
209
+
210
+ # List all manual fastify routes
211
+ grep -rnE "fastify\\.(get|post|patch|put|delete)\\(" src/
212
+
213
+ # Find driver imports outside adapter dirs
214
+ grep -rln "from 'mongoose'\\|from '@prisma/client'\\|from 'drizzle-orm'" src/ \
215
+ | grep -v 'adapter\\|\\.model\\.'
216
+
217
+ # Check for hand-rolled toJSON
218
+ grep -rn "schema.set('toJSON'\\|toJSON = function" src/
219
+
220
+ # Count manual permission checks
221
+ grep -rcE "user\\.role|roles\\.includes|throw.*(Unauthorized|Forbidden)" src/
222
+
223
+ # Verify OpenAPI source of truth
224
+ ls -la openapi.* swagger.* api-spec.* 2>/dev/null
225
+
226
+ # Check arc CLI is wired
227
+ grep -E "arc (init|generate|docs)" package.json
228
+ ```
229
+
230
+ Use the counts to fill in the per-resource scorecard in the SKILL.md report template.