@prmichaelsen/remember-mcp 0.1.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.
- package/.env.example +65 -0
- package/AGENT.md +840 -0
- package/README.md +72 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/access-control-result-pattern.md +458 -0
- package/agent/design/action-audit-memory-types.md +637 -0
- package/agent/design/common-template-fields.md +282 -0
- package/agent/design/complete-tool-set.md +407 -0
- package/agent/design/content-types-expansion.md +521 -0
- package/agent/design/cross-database-id-strategy.md +358 -0
- package/agent/design/default-template-library.md +423 -0
- package/agent/design/firestore-wrapper-analysis.md +606 -0
- package/agent/design/llm-provider-abstraction.md +691 -0
- package/agent/design/location-handling-architecture.md +523 -0
- package/agent/design/memory-templates-design.md +364 -0
- package/agent/design/permissions-storage-architecture.md +680 -0
- package/agent/design/relationship-storage-strategy.md +361 -0
- package/agent/design/remember-mcp-implementation-tasks.md +417 -0
- package/agent/design/remember-mcp-progress.yaml +141 -0
- package/agent/design/requirements-enhancements.md +468 -0
- package/agent/design/requirements.md +56 -0
- package/agent/design/template-storage-strategy.md +412 -0
- package/agent/design/template-suggestion-system.md +853 -0
- package/agent/design/trust-escalation-prevention.md +343 -0
- package/agent/design/trust-system-implementation.md +592 -0
- package/agent/design/user-preferences.md +683 -0
- package/agent/design/weaviate-collection-strategy.md +461 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-foundation.md +121 -0
- package/agent/milestones/milestone-2-core-memory-system.md +150 -0
- package/agent/milestones/milestone-3-relationships-graph.md +116 -0
- package/agent/milestones/milestone-4-user-preferences.md +103 -0
- package/agent/milestones/milestone-5-template-system.md +126 -0
- package/agent/milestones/milestone-6-auth-multi-tenancy.md +124 -0
- package/agent/milestones/milestone-7-trust-permissions.md +133 -0
- package/agent/milestones/milestone-8-testing-quality.md +137 -0
- package/agent/milestones/milestone-9-deployment-documentation.md +147 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.md +1271 -0
- package/agent/patterns/firebase-admin-sdk-v8-usage.md +950 -0
- package/agent/patterns/firestore-users-pattern-best-practices.md +347 -0
- package/agent/patterns/library-services.md +454 -0
- package/agent/patterns/testing-colocated.md +316 -0
- package/agent/progress.yaml +395 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/task-1-initialize-project-structure.md +266 -0
- package/agent/tasks/task-2-install-dependencies.md +199 -0
- package/agent/tasks/task-3-setup-weaviate-client.md +330 -0
- package/agent/tasks/task-4-setup-firestore-client.md +362 -0
- package/agent/tasks/task-5-create-basic-mcp-server.md +114 -0
- package/agent/tasks/task-6-create-integration-tests.md +195 -0
- package/agent/tasks/task-7-finalize-milestone-1.md +363 -0
- package/agent/tasks/task-8-setup-utility-scripts.md +382 -0
- package/agent/tasks/task-9-create-server-factory.md +404 -0
- package/dist/config.d.ts +26 -0
- package/dist/constants/content-types.d.ts +60 -0
- package/dist/firestore/init.d.ts +14 -0
- package/dist/firestore/paths.d.ts +53 -0
- package/dist/firestore/paths.spec.d.ts +2 -0
- package/dist/server-factory.d.ts +40 -0
- package/dist/server-factory.js +1741 -0
- package/dist/server-factory.spec.d.ts +2 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +1690 -0
- package/dist/tools/create-memory.d.ts +94 -0
- package/dist/tools/delete-memory.d.ts +47 -0
- package/dist/tools/search-memory.d.ts +88 -0
- package/dist/types/memory.d.ts +183 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/weaviate/client.d.ts +39 -0
- package/dist/weaviate/client.spec.d.ts +2 -0
- package/dist/weaviate/schema.d.ts +29 -0
- package/esbuild.build.js +60 -0
- package/esbuild.watch.js +25 -0
- package/jest.config.js +31 -0
- package/jest.e2e.config.js +17 -0
- package/package.json +68 -0
- package/src/.gitkeep +0 -0
- package/src/config.ts +56 -0
- package/src/constants/content-types.ts +454 -0
- package/src/firestore/init.ts +68 -0
- package/src/firestore/paths.spec.ts +75 -0
- package/src/firestore/paths.ts +124 -0
- package/src/server-factory.spec.ts +60 -0
- package/src/server-factory.ts +215 -0
- package/src/server.ts +243 -0
- package/src/tools/create-memory.ts +198 -0
- package/src/tools/delete-memory.ts +126 -0
- package/src/tools/search-memory.ts +216 -0
- package/src/types/memory.ts +276 -0
- package/src/utils/logger.ts +42 -0
- package/src/weaviate/client.spec.ts +58 -0
- package/src/weaviate/client.ts +114 -0
- package/src/weaviate/schema.ts +288 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# Template Storage Strategy
|
|
2
|
+
|
|
3
|
+
**Concept**: Hybrid storage for user templates and default template library
|
|
4
|
+
**Created**: 2026-02-11
|
|
5
|
+
**Updated**: 2026-02-11 (Simplified - no sharing)
|
|
6
|
+
**Status**: Design Specification (FINAL)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Design Decision: No Template Sharing
|
|
11
|
+
|
|
12
|
+
**Key Decision**: Templates are either **default** (available to all) or **private** (user-only). No sharing between users.
|
|
13
|
+
|
|
14
|
+
**Rationale**:
|
|
15
|
+
- Avoids permission bloat (thousands of permission docs per popular template)
|
|
16
|
+
- Simpler security rules and queries
|
|
17
|
+
- Better scalability
|
|
18
|
+
- If a user template is good, platform promotes it to default library
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Firestore Structure (FINAL)
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
firestore/
|
|
26
|
+
├── templates/
|
|
27
|
+
│ └── default/
|
|
28
|
+
│ └── {weaviate_uuid}/ # Default templates (platform-curated)
|
|
29
|
+
│ ├── template_id: weaviate_uuid
|
|
30
|
+
│ ├── template_name
|
|
31
|
+
│ ├── category
|
|
32
|
+
│ ├── is_default: true
|
|
33
|
+
│ ├── is_immutable: true
|
|
34
|
+
│ ├── usage_count
|
|
35
|
+
│ └── version
|
|
36
|
+
│
|
|
37
|
+
└── users/
|
|
38
|
+
└── {user_id}/
|
|
39
|
+
├── preferences/ # User preferences
|
|
40
|
+
└── templates/
|
|
41
|
+
└── {weaviate_uuid}/ # User's custom templates
|
|
42
|
+
├── template_id: weaviate_uuid
|
|
43
|
+
├── owner_user_id
|
|
44
|
+
├── template_name
|
|
45
|
+
├── derived_from # If copied from default
|
|
46
|
+
├── usage_count
|
|
47
|
+
└── created_at
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Benefits**:
|
|
51
|
+
- ✅ Simple, clear structure
|
|
52
|
+
- ✅ No permission bloat
|
|
53
|
+
- ✅ Easy to query user's templates: `users/{user_id}/templates/`
|
|
54
|
+
- ✅ Easy to query default templates: `templates/default/`
|
|
55
|
+
- ✅ Scales well (no per-user permission docs)
|
|
56
|
+
- ✅ Clear ownership model
|
|
57
|
+
- ✅ Reuses Weaviate UUID as Firestore doc ID
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Weaviate Collections
|
|
62
|
+
|
|
63
|
+
### Template Collections
|
|
64
|
+
|
|
65
|
+
**1. Template_system** - Default templates (shared across all users)
|
|
66
|
+
- Platform-curated templates
|
|
67
|
+
- Immutable
|
|
68
|
+
- Available to all users
|
|
69
|
+
- Semantic search enabled
|
|
70
|
+
|
|
71
|
+
**2. Template_{user_id}** - User-specific templates
|
|
72
|
+
- Created by user
|
|
73
|
+
- Private to owner
|
|
74
|
+
- Modifiable
|
|
75
|
+
- Semantic search enabled
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Template Visibility Model
|
|
80
|
+
|
|
81
|
+
### Two Types Only
|
|
82
|
+
|
|
83
|
+
**1. Default Templates** (`templates/default/{weaviate_uuid}`)
|
|
84
|
+
- Created by platform
|
|
85
|
+
- Available to ALL users
|
|
86
|
+
- Immutable (users can't modify)
|
|
87
|
+
- Users can copy to customize
|
|
88
|
+
- Examples: Person Profile, Meeting Notes, Inventory Item
|
|
89
|
+
|
|
90
|
+
**2. User Templates** (`users/{user_id}/templates/{weaviate_uuid}`)
|
|
91
|
+
- Created by user
|
|
92
|
+
- Private to that user only
|
|
93
|
+
- User can modify freely
|
|
94
|
+
- Can be derived from default templates
|
|
95
|
+
|
|
96
|
+
**No Sharing Between Users**: If a user template is valuable, platform promotes it to default library
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Implementation
|
|
101
|
+
|
|
102
|
+
### Create Default Template
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
async function createDefaultTemplate(template: Template): Promise<string> {
|
|
106
|
+
// 1. Create in Weaviate (generates UUID)
|
|
107
|
+
const templateId = await weaviateClient
|
|
108
|
+
.collection('Template_system')
|
|
109
|
+
.data.insert({
|
|
110
|
+
...template,
|
|
111
|
+
is_default: true,
|
|
112
|
+
is_immutable: true,
|
|
113
|
+
created_at: new Date()
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 2. Create in Firestore (reuse Weaviate UUID)
|
|
117
|
+
await firestore
|
|
118
|
+
.collection('templates')
|
|
119
|
+
.doc('default')
|
|
120
|
+
.collection('templates')
|
|
121
|
+
.doc(templateId) // ✅ Reuse Weaviate UUID
|
|
122
|
+
.set({
|
|
123
|
+
template_id: templateId,
|
|
124
|
+
template_name: template.template_name,
|
|
125
|
+
category: template.category,
|
|
126
|
+
is_default: true,
|
|
127
|
+
is_immutable: true,
|
|
128
|
+
usage_count: 0,
|
|
129
|
+
version: "1.0",
|
|
130
|
+
created_at: Timestamp.now()
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return templateId;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Create User Template
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
async function createUserTemplate(
|
|
141
|
+
template: Template,
|
|
142
|
+
user_id: string
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
// 1. Create in Weaviate (generates UUID)
|
|
145
|
+
const templateId = await weaviateClient
|
|
146
|
+
.collection(`Template_${user_id}`)
|
|
147
|
+
.data.insert({
|
|
148
|
+
...template,
|
|
149
|
+
owner_user_id: user_id,
|
|
150
|
+
is_default: false,
|
|
151
|
+
is_immutable: false,
|
|
152
|
+
created_at: new Date()
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 2. Create in Firestore (reuse Weaviate UUID)
|
|
156
|
+
await firestore
|
|
157
|
+
.collection('users')
|
|
158
|
+
.doc(user_id)
|
|
159
|
+
.collection('templates')
|
|
160
|
+
.doc(templateId) // ✅ Reuse Weaviate UUID
|
|
161
|
+
.set({
|
|
162
|
+
template_id: templateId,
|
|
163
|
+
owner_user_id: user_id,
|
|
164
|
+
template_name: template.template_name,
|
|
165
|
+
derived_from: template.derived_from || null,
|
|
166
|
+
usage_count: 0,
|
|
167
|
+
created_at: Timestamp.now()
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return templateId;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Copy Default Template
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
async function copyDefaultTemplate(
|
|
178
|
+
source_template_id: string,
|
|
179
|
+
user_id: string,
|
|
180
|
+
customizations?: Partial<Template>
|
|
181
|
+
): Promise<string> {
|
|
182
|
+
// Get default template from Weaviate
|
|
183
|
+
const defaultTemplate = await weaviateClient
|
|
184
|
+
.collection('Template_system')
|
|
185
|
+
.data.getById(source_template_id);
|
|
186
|
+
|
|
187
|
+
// Create user's copy
|
|
188
|
+
return await createUserTemplate({
|
|
189
|
+
...defaultTemplate.properties,
|
|
190
|
+
...customizations,
|
|
191
|
+
derived_from: source_template_id
|
|
192
|
+
}, user_id);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Query Templates
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Get user's templates
|
|
200
|
+
async function getUserTemplates(user_id: string): Promise<Template[]> {
|
|
201
|
+
const snapshot = await firestore
|
|
202
|
+
.collection('users')
|
|
203
|
+
.doc(user_id)
|
|
204
|
+
.collection('templates')
|
|
205
|
+
.get();
|
|
206
|
+
|
|
207
|
+
const templateIds = snapshot.docs.map(doc => doc.id);
|
|
208
|
+
return await fetchTemplatesFromWeaviate(templateIds, `Template_${user_id}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Get default templates
|
|
212
|
+
async function getDefaultTemplates(): Promise<Template[]> {
|
|
213
|
+
const snapshot = await firestore
|
|
214
|
+
.collectionGroup('templates') // Query across all default templates
|
|
215
|
+
.where('is_default', '==', true)
|
|
216
|
+
.get();
|
|
217
|
+
|
|
218
|
+
const templateIds = snapshot.docs.map(doc => doc.id);
|
|
219
|
+
return await fetchTemplatesFromWeaviate(templateIds, 'Template_system');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Get all available templates for user
|
|
223
|
+
async function getAllAvailableTemplates(user_id: string): Promise<Template[]> {
|
|
224
|
+
const [defaults, userTemplates] = await Promise.all([
|
|
225
|
+
getDefaultTemplates(),
|
|
226
|
+
getUserTemplates(user_id)
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
return [...defaults, ...userTemplates];
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Template Promotion Model
|
|
236
|
+
|
|
237
|
+
### User Template → Default Template
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
async function promoteToDefault(
|
|
241
|
+
user_template_id: string,
|
|
242
|
+
user_id: string,
|
|
243
|
+
admin_user_id: string,
|
|
244
|
+
reason: string
|
|
245
|
+
): Promise<string> {
|
|
246
|
+
// 1. Get user template
|
|
247
|
+
const userTemplate = await weaviateClient
|
|
248
|
+
.collection(`Template_${user_id}`)
|
|
249
|
+
.data.getById(user_template_id);
|
|
250
|
+
|
|
251
|
+
// 2. Create as default template
|
|
252
|
+
const defaultTemplateId = await createDefaultTemplate({
|
|
253
|
+
...userTemplate.properties,
|
|
254
|
+
promoted_from: user_template_id,
|
|
255
|
+
original_author: user_id,
|
|
256
|
+
promoted_by: admin_user_id,
|
|
257
|
+
promoted_at: new Date(),
|
|
258
|
+
promotion_reason: reason
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// 3. Notify original author
|
|
262
|
+
await notifyTemplatePromotion(user_id, {
|
|
263
|
+
user_template_id,
|
|
264
|
+
default_template_id: defaultTemplateId,
|
|
265
|
+
message: "Your template has been promoted to the default library!"
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return defaultTemplateId;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Firestore Security Rules
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
rules_version = '2';
|
|
278
|
+
service cloud.firestore {
|
|
279
|
+
match /databases/{database}/documents {
|
|
280
|
+
|
|
281
|
+
// Default templates - read-only for all authenticated users
|
|
282
|
+
match /templates/default/{template_id} {
|
|
283
|
+
allow read: if request.auth != null;
|
|
284
|
+
allow write: if false; // Only via admin SDK
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// User templates - full control for owner only
|
|
288
|
+
match /users/{user_id}/templates/{template_id} {
|
|
289
|
+
allow read, write: if request.auth.uid == user_id;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// User preferences
|
|
293
|
+
match /users/{user_id}/preferences {
|
|
294
|
+
allow read, write: if request.auth.uid == user_id;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Default Template Library (15 Templates)
|
|
303
|
+
|
|
304
|
+
### Core Templates
|
|
305
|
+
|
|
306
|
+
1. **Person Profile** - Track people you meet (with `how_we_met` field)
|
|
307
|
+
2. **Professional Contact** - Business contacts
|
|
308
|
+
3. **Meeting Notes** - Meeting documentation
|
|
309
|
+
4. **Restaurant Review** - Dining experiences
|
|
310
|
+
5. **Book Review** - Books read
|
|
311
|
+
6. **Movie Review** - Movies watched
|
|
312
|
+
7. **Recipe** - Cooking recipes
|
|
313
|
+
8. **Travel Destination** - Places visited
|
|
314
|
+
9. **Project Tracker** - Project management
|
|
315
|
+
10. **Goal Tracker** - Personal/professional goals
|
|
316
|
+
11. **Habit Tracker** - Daily habits
|
|
317
|
+
12. **Inventory Item** - Home organization (NEW)
|
|
318
|
+
13. **Checklist Template** - Reusable checklists
|
|
319
|
+
14. **Journal Entry** - Daily journaling
|
|
320
|
+
15. **Idea Capture** - Quick ideas and brainstorms
|
|
321
|
+
|
|
322
|
+
### Template Categories
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const TEMPLATE_CATEGORIES = {
|
|
326
|
+
contacts: ['person_profile', 'professional_contact'],
|
|
327
|
+
work: ['meeting_notes', 'project_tracker'],
|
|
328
|
+
personal: ['journal_entry', 'goal_tracker', 'habit_tracker'],
|
|
329
|
+
entertainment: ['book_review', 'movie_review', 'restaurant_review'],
|
|
330
|
+
organization: ['inventory_item', 'checklist_template'],
|
|
331
|
+
creative: ['recipe', 'idea_capture'],
|
|
332
|
+
travel: ['travel_destination']
|
|
333
|
+
};
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Benefits of Simplified Approach
|
|
339
|
+
|
|
340
|
+
### 1. **Scalability**
|
|
341
|
+
- No permission documents per user
|
|
342
|
+
- Avoids permission bloat
|
|
343
|
+
- Simple, predictable queries
|
|
344
|
+
- Firestore costs stay low
|
|
345
|
+
|
|
346
|
+
### 2. **Clarity**
|
|
347
|
+
- Clear ownership model
|
|
348
|
+
- Either default (all) or private (owner)
|
|
349
|
+
- No complex sharing logic
|
|
350
|
+
- Easy to understand
|
|
351
|
+
|
|
352
|
+
### 3. **Performance**
|
|
353
|
+
- No permission checks needed
|
|
354
|
+
- Faster queries
|
|
355
|
+
- Less Firestore reads
|
|
356
|
+
- Simpler caching
|
|
357
|
+
|
|
358
|
+
### 4. **Maintainability**
|
|
359
|
+
- Simpler code
|
|
360
|
+
- Fewer edge cases
|
|
361
|
+
- Easier to reason about
|
|
362
|
+
- Less testing needed
|
|
363
|
+
|
|
364
|
+
### 5. **Quality Control**
|
|
365
|
+
- Platform curates defaults
|
|
366
|
+
- Ensures template quality
|
|
367
|
+
- Users get credit for contributions
|
|
368
|
+
- Community benefits from best templates
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Comparison
|
|
373
|
+
|
|
374
|
+
### ❌ Complex (With Sharing - Rejected)
|
|
375
|
+
```
|
|
376
|
+
templates/{template_id}/
|
|
377
|
+
├── metadata
|
|
378
|
+
└── permissions/
|
|
379
|
+
├── {user_1}/ # Can use
|
|
380
|
+
├── {user_2}/ # Can use
|
|
381
|
+
├── {user_3}/ # Can use
|
|
382
|
+
└── ... (could be thousands of permission docs!)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Problems**:
|
|
386
|
+
- Permission bloat for popular templates
|
|
387
|
+
- Complex queries
|
|
388
|
+
- Expensive Firestore reads
|
|
389
|
+
- Hard to maintain
|
|
390
|
+
|
|
391
|
+
### ✅ Simple (No Sharing - Accepted)
|
|
392
|
+
```
|
|
393
|
+
templates/default/{weaviate_uuid}/
|
|
394
|
+
└── metadata (available to all)
|
|
395
|
+
|
|
396
|
+
users/{user_id}/templates/{weaviate_uuid}/
|
|
397
|
+
└── metadata (owner only)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Benefits**:
|
|
401
|
+
- No permission documents
|
|
402
|
+
- Simple queries
|
|
403
|
+
- Scalable
|
|
404
|
+
- Clear ownership
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
**Status**: Design Specification (FINAL)
|
|
409
|
+
**Structure**: `templates/default/` and `users/{user_id}/templates/`
|
|
410
|
+
**Sharing**: Not supported - use promotion model instead
|
|
411
|
+
**ID Strategy**: Reuse Weaviate UUID as Firestore document ID
|
|
412
|
+
**Benefit**: Simpler, more scalable, easier to maintain
|