parse-stack-next 5.0.0 → 5.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
  5. data/.github/dependabot.yml +13 -0
  6. data/.github/workflows/codeql.yml +1 -1
  7. data/.github/workflows/docs.yml +3 -3
  8. data/.github/workflows/release.yml +43 -0
  9. data/.github/workflows/ruby.yml +1 -1
  10. data/.gitignore +1 -0
  11. data/.vscode/settings.json +3 -0
  12. data/.yardopts +19 -0
  13. data/CHANGELOG.md +802 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +8 -5
  16. data/README.md +16 -1
  17. data/Rakefile +5 -1
  18. data/docs/acl_clp_guide.md +553 -0
  19. data/docs/atlas_vector_search_guide.md +123 -22
  20. data/docs/client_sdk_guide.md +201 -5
  21. data/docs/usage_guide.md +21 -0
  22. data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
  23. data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
  24. data/lib/parse/agent/tools.rb +153 -1
  25. data/lib/parse/cache/pool.rb +15 -0
  26. data/lib/parse/cache/redis.rb +114 -2
  27. data/lib/parse/client/caching.rb +18 -1
  28. data/lib/parse/client.rb +79 -12
  29. data/lib/parse/embeddings/cohere.rb +143 -6
  30. data/lib/parse/embeddings/provider.rb +20 -2
  31. data/lib/parse/embeddings/voyage.rb +102 -0
  32. data/lib/parse/embeddings.rb +332 -1
  33. data/lib/parse/live_query/client.rb +167 -4
  34. data/lib/parse/live_query/configuration.rb +12 -0
  35. data/lib/parse/live_query/subscription.rb +55 -2
  36. data/lib/parse/live_query.rb +123 -1
  37. data/lib/parse/lock.rb +342 -0
  38. data/lib/parse/lock_backend.rb +308 -0
  39. data/lib/parse/model/classes/audience.rb +5 -0
  40. data/lib/parse/model/classes/installation.rb +122 -0
  41. data/lib/parse/model/classes/job_schedule.rb +3 -1
  42. data/lib/parse/model/classes/job_status.rb +4 -1
  43. data/lib/parse/model/classes/push_status.rb +4 -1
  44. data/lib/parse/model/classes/session.rb +7 -0
  45. data/lib/parse/model/classes/user.rb +204 -0
  46. data/lib/parse/model/core/create_lock.rb +28 -134
  47. data/lib/parse/model/core/embed_managed.rb +162 -13
  48. data/lib/parse/model/core/parse_reference.rb +17 -1
  49. data/lib/parse/model/core/querying.rb +26 -2
  50. data/lib/parse/model/file.rb +523 -18
  51. data/lib/parse/query.rb +31 -1
  52. data/lib/parse/stack/version.rb +1 -1
  53. data/lib/parse/stack.rb +98 -1
  54. data/parse-stack-next.gemspec +2 -2
  55. metadata +19 -7
@@ -0,0 +1,387 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif;
4
+ font-size: 13px;
5
+ height: 101%;
6
+ overflow-x: hidden;
7
+ background: #fafafa;
8
+ }
9
+
10
+ h1 {
11
+ padding: 12px 10px;
12
+ padding-bottom: 0;
13
+ margin: 0;
14
+ font-size: 1.4em;
15
+ }
16
+ .clear {
17
+ clear: both;
18
+ }
19
+ .fixed_header {
20
+ position: fixed;
21
+ background: #fff;
22
+ width: 100%;
23
+ padding-bottom: 10px;
24
+ margin-top: 0;
25
+ top: 0;
26
+ z-index: 9999;
27
+ height: 70px;
28
+ }
29
+ #search {
30
+ position: absolute;
31
+ right: 5px;
32
+ top: 9px;
33
+ padding-left: 24px;
34
+ }
35
+ #noresults {
36
+ padding: 7px 12px;
37
+ background: #fff;
38
+ }
39
+ #content.insearch #search,
40
+ #content.insearch #noresults {
41
+ background: url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///wAAAPr6+pKSkoiIiO7u7sjIyNjY2J6engAAAI6OjsbGxjIyMlJSUuzs7KamppSUlPLy8oKCghwcHLKysqSkpJqamvT09Pj4+KioqM7OzkRERAwMDGBgYN7e3ujo6Ly8vCoqKjY2NkZGRtTU1MTExDw8PE5OTj4+PkhISNDQ0MrKylpaWrS0tOrq6nBwcKysrLi4uLq6ul5eXlxcXGJiYoaGhuDg4H5+fvz8/KKiohgYGCwsLFZWVgQEBFBQUMzMzDg4OFhYWBoaGvDw8NbW1pycnOLi4ubm5kBAQKqqqiQkJCAgIK6urnJyckpKSjQ0NGpqatLS0sDAwCYmJnx8fEJCQlRUVAoKCggICLCwsOTk5ExMTPb29ra2tmZmZmhoaNzc3KCgoBISEiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCAAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkIAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQgAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkIAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==)
42
+ no-repeat center left;
43
+ }
44
+ #full_list {
45
+ padding: 0;
46
+ list-style: none;
47
+ margin-left: 0;
48
+ margin-top: 80px;
49
+ font-size: 1.1em;
50
+ }
51
+ #full_list ul {
52
+ padding: 0;
53
+ }
54
+ #full_list li {
55
+ padding: 0;
56
+ margin: 0;
57
+ list-style: none;
58
+ }
59
+ #full_list li .item {
60
+ padding: 5px 5px 5px 12px;
61
+ }
62
+ #content.insearch #noresults {
63
+ margin-left: 7px;
64
+ }
65
+ #full_list li {
66
+ color: #666;
67
+ cursor: normal;
68
+ white-space: nowrap;
69
+ }
70
+ #full_list li.collapsed ul {
71
+ display: none;
72
+ }
73
+ #full_list li a.toggle {
74
+ cursor: default;
75
+ position: relative;
76
+ left: -5px;
77
+ top: 4px;
78
+ text-indent: -999px;
79
+ width: 10px;
80
+ height: 9px;
81
+ margin-left: -10px;
82
+ display: block;
83
+ float: left;
84
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMy8xNC8wOeNZPpQAAAE2SURBVDiNrZTBccIwEEXfelIAHUA6CZ24BGaWO+FuzZAK4k6gg5QAdGAq+Bxs2Yqx7BzyL7Llp/VfzZeQhCTc/ezuGzKKnKSzpCxXJM8fwNXda3df5RZETlIt6YUzSQDs93sl8w3wBZxCCE10GM1OcWbWjB2mWgEH4Mfdyxm3PSepBHibgQE2wLe7r4HjEidpnXMYdQPKEMJcsZ4zs2POYQOcaPfwMVOo58zsAdMt18BuoVDPxUJRacELbXv3hUIX2vYmOUvi8C8ydz/ThjXrqKqqLbDIAdsCKBd+Wo7GWa7o9qzOQHVVVXeAbs+yHHCH4aTsaCOQqunmUy1yBUAXkdMIfMlgF5EXLo2OpV/c/Up7jG4hhHcYLgWzAZXUc2b2ixsfvc/RmNNfOXD3Q/oeL9axJE1yT9IOoUu6MGUkAAAAAElFTkSuQmCC)
85
+ no-repeat bottom left;
86
+ }
87
+ #full_list li.collapsed a.toggle {
88
+ cursor: default;
89
+ background-position: top left;
90
+ }
91
+ #full_list li.deprecated {
92
+ text-decoration: line-through;
93
+ font-style: italic;
94
+ }
95
+ #full_list li.odd {
96
+ background: #f0f0f0;
97
+ }
98
+ #full_list li.even {
99
+ background: #fafafa;
100
+ }
101
+ #full_list .item:hover {
102
+ background: #ddd;
103
+ }
104
+ #full_list li small:before {
105
+ content: "(";
106
+ }
107
+ #full_list li small:after {
108
+ content: ")";
109
+ }
110
+ #full_list li small.search_info {
111
+ display: none;
112
+ }
113
+ a,
114
+ a:visited {
115
+ text-decoration: none;
116
+ color: #05a;
117
+ }
118
+ #full_list li.clicked > .item {
119
+ background: #05a;
120
+ color: #ccc;
121
+ }
122
+ #full_list li.clicked > .item a,
123
+ #full_list li.clicked > .item a:visited {
124
+ color: #eee;
125
+ }
126
+ #full_list li.clicked > .item a.toggle {
127
+ opacity: 0.5;
128
+ background-position: bottom right;
129
+ }
130
+ #full_list li.collapsed.clicked a.toggle {
131
+ background-position: top right;
132
+ }
133
+ #search input {
134
+ border: 1px solid #bbb;
135
+ border-radius: 3px;
136
+ }
137
+ #full_list_nav {
138
+ margin-left: 10px;
139
+ font-size: 0.9em;
140
+ display: block;
141
+ color: #aaa;
142
+ }
143
+ #full_list_nav a,
144
+ #nav a:visited {
145
+ color: #358;
146
+ }
147
+ #full_list_nav a:hover {
148
+ background: transparent;
149
+ color: #5af;
150
+ }
151
+ #full_list_nav span:after {
152
+ content: " | ";
153
+ }
154
+ #full_list_nav span:last-child:after {
155
+ content: "";
156
+ }
157
+
158
+ #content h1 {
159
+ margin-top: 0;
160
+ }
161
+ #full_list li small {
162
+ display: block;
163
+ font-size: 0.8em;
164
+ }
165
+ #full_list li small:before {
166
+ content: "";
167
+ }
168
+ #full_list li small:after {
169
+ content: "";
170
+ }
171
+ #full_list li small.search_info {
172
+ display: none;
173
+ }
174
+ #search {
175
+ width: 170px;
176
+ position: static;
177
+ margin: 3px;
178
+ margin-left: 10px;
179
+ font-size: 0.9em;
180
+ color: #666;
181
+ padding-left: 0;
182
+ padding-right: 24px;
183
+ }
184
+ #content.insearch #search {
185
+ background-position: center right;
186
+ }
187
+ #search input {
188
+ width: 110px;
189
+ }
190
+
191
+ #full_list.insearch ul {
192
+ display: block;
193
+ }
194
+ #full_list.insearch .item {
195
+ display: none;
196
+ }
197
+ #full_list.insearch .found {
198
+ display: block;
199
+ padding-left: 11px;
200
+ }
201
+ #full_list.insearch li a.toggle {
202
+ display: none;
203
+ }
204
+ #full_list.insearch li small.search_info {
205
+ display: block;
206
+ }
207
+
208
+ /* ============================================================
209
+ * parse-stack-next overlay (sidebar / class-list iframe)
210
+ * ============================================================ */
211
+
212
+ :root {
213
+ --ps-bg: #1E1F22;
214
+ --ps-bg-soft: #2A2C30;
215
+ --ps-bg-hover: #34373C;
216
+ --ps-fg: #E6E8EB;
217
+ --ps-fg-muted: #9CA3AF;
218
+ --ps-accent: #E63946;
219
+ --ps-link: #4FB8F4;
220
+ --ps-border: #3A3D42;
221
+ --ps-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter",
222
+ Roboto, "Helvetica Neue", Arial, sans-serif;
223
+ }
224
+
225
+ body {
226
+ background: var(--ps-bg) !important;
227
+ color: var(--ps-fg);
228
+ font-family: var(--ps-font);
229
+ font-size: 13px;
230
+ }
231
+
232
+ h1 {
233
+ color: var(--ps-fg);
234
+ font-weight: 650;
235
+ letter-spacing: -0.01em;
236
+ }
237
+
238
+ .fixed_header {
239
+ background: var(--ps-bg-soft) !important;
240
+ border-bottom: 1px solid var(--ps-border);
241
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
242
+ }
243
+
244
+ #search input,
245
+ #full_list_search input {
246
+ background: var(--ps-bg) !important;
247
+ color: var(--ps-fg) !important;
248
+ border: 1px solid var(--ps-border) !important;
249
+ border-radius: 4px;
250
+ padding: 4px 8px;
251
+ font-family: var(--ps-font);
252
+ }
253
+
254
+ #search input::placeholder { color: var(--ps-fg-muted); }
255
+
256
+ #full_list li,
257
+ #full_list li .item {
258
+ color: var(--ps-fg);
259
+ border: 0;
260
+ }
261
+
262
+ #full_list li.odd { background: transparent; }
263
+ #full_list li.even { background: rgba(255, 255, 255, .025); }
264
+
265
+ #full_list li a,
266
+ #full_list li .item a,
267
+ #full_list li a:visited,
268
+ #full_list li .item a:visited {
269
+ color: var(--ps-fg);
270
+ }
271
+
272
+ #full_list .item:hover,
273
+ #full_list li:hover,
274
+ #full_list li:hover .item {
275
+ background: var(--ps-bg-hover) !important;
276
+ color: var(--ps-fg);
277
+ }
278
+
279
+ #full_list li.clicked > .item,
280
+ #full_list li.clicked > .item a,
281
+ #full_list li.clicked > .item a:visited {
282
+ background: var(--ps-accent) !important;
283
+ color: #fff !important;
284
+ border-color: var(--ps-accent) !important;
285
+ }
286
+
287
+ #full_list li small { color: var(--ps-fg-muted); }
288
+ #full_list_nav { color: var(--ps-fg-muted); }
289
+
290
+ #full_list_nav a,
291
+ #full_list_nav a:visited { color: var(--ps-link); }
292
+ #full_list_nav a:hover { color: var(--ps-accent); }
293
+
294
+ #noresults {
295
+ background: var(--ps-bg-soft);
296
+ color: var(--ps-fg-muted);
297
+ border-radius: 4px;
298
+ }
299
+
300
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
301
+ ::-webkit-scrollbar-track { background: var(--ps-bg); }
302
+ ::-webkit-scrollbar-thumb { background: #4a4d52; border-radius: 4px; }
303
+ ::-webkit-scrollbar-thumb:hover { background: var(--ps-link); }
304
+
305
+ /* ============================================================
306
+ * Re-pin iframe-only layout values that common.css would otherwise
307
+ * stomp on.
308
+ *
309
+ * Both stylesheets are loaded inside the class-list iframe AND the
310
+ * main page. common.css generously styles tags like h1, #content,
311
+ * #header for the main page, where their layout role is "big page
312
+ * heading" / "main content column" / "top nav". Inside the iframe
313
+ * those same selectors mean "Class List" / "sidebar viewport" /
314
+ * "fixed top strip", and the sizes need to be drastically smaller.
315
+ * Re-pin them explicitly here so the iframe doesn't inherit the
316
+ * main-page proportions.
317
+ * ============================================================ */
318
+
319
+ #full_list_header,
320
+ body > #content > .fixed_header > h1,
321
+ .fixed_header h1 {
322
+ font-size: 1.4em !important;
323
+ font-weight: 650;
324
+ padding: 12px 10px 4px !important;
325
+ margin: 0 !important;
326
+ border: 0 !important;
327
+ display: block !important;
328
+ color: var(--ps-fg);
329
+ letter-spacing: -0.01em;
330
+ line-height: 1.2;
331
+ }
332
+
333
+ /* Reserve clearance for the fixed_header. With the larger header
334
+ * font + the flex search row, the stock 80px margin isn't enough. */
335
+ body > #content > #full_list {
336
+ margin-top: 120px !important;
337
+ }
338
+
339
+ /* The iframe's #content is just a passive scroll container, not the
340
+ * main-page article column. Wipe any main-page color/padding rules. */
341
+ body > #content {
342
+ background: var(--ps-bg) !important;
343
+ color: var(--ps-fg);
344
+ padding: 0;
345
+ margin: 0;
346
+ }
347
+
348
+ /* Top nav strip: "Classes | Methods | Files" links. */
349
+ #full_list_nav {
350
+ padding: 0 10px;
351
+ font-size: 0.85em;
352
+ color: var(--ps-fg-muted);
353
+ }
354
+
355
+ /* Search row inside the fixed header — keep it on one line. */
356
+ .fixed_header #search {
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 6px;
360
+ padding: 4px 10px 0;
361
+ width: auto;
362
+ }
363
+
364
+ .fixed_header #search label {
365
+ color: var(--ps-fg-muted);
366
+ font-size: 0.85em;
367
+ }
368
+
369
+ .fixed_header #search input {
370
+ flex: 1 1 auto;
371
+ min-width: 0;
372
+ }
373
+
374
+ /* Belt-and-braces: the fixed strip must be opaque and above the list. */
375
+ .fixed_header {
376
+ background: var(--ps-bg-soft) !important;
377
+ z-index: 9999;
378
+ height: auto;
379
+ min-height: 70px;
380
+ padding-bottom: 8px;
381
+ }
382
+
383
+ /* The collapse toggle PNG is a dark-on-transparent sprite; invert it
384
+ * so the arrow shows on dark backgrounds. */
385
+ #full_list li a.toggle {
386
+ filter: invert(0.85) hue-rotate(180deg) brightness(1.6);
387
+ }
@@ -469,6 +469,35 @@ module Parse
469
469
  },
470
470
  required: ["class_name", "pipeline"],
471
471
  },
472
+ output_schema: {
473
+ type: "object",
474
+ properties: {
475
+ class_name: { type: "string" },
476
+ pipeline_stages: { type: "integer", minimum: 0 },
477
+ result_count: { type: "integer", minimum: 0 },
478
+ # `route` is :mongo_direct or :parse_server but serializes
479
+ # to a Symbol-shaped String in JSON envelopes; declare it
480
+ # permissively as string.
481
+ route: { type: "string", description: "Routing tag: 'mongo_direct' or 'parse_server'." },
482
+ # Aggregation result rows are class-shape-dependent and may
483
+ # be the output of arbitrary $project / $group / $lookup
484
+ # stages. Object envelopes with open property sets are the
485
+ # honest representation.
486
+ results: {
487
+ type: "array",
488
+ items: { type: "object", additionalProperties: true },
489
+ },
490
+ pointer_classes: {
491
+ type: "object",
492
+ additionalProperties: { type: "string" },
493
+ description: "Optional. Field-name → Parse-class-name map when compact_pointers is on.",
494
+ },
495
+ auto_limited: { type: "boolean" },
496
+ auto_limit: { type: "integer", minimum: 1 },
497
+ hint: { type: "string" },
498
+ },
499
+ required: %w[class_name pipeline_stages result_count route results],
500
+ },
472
501
  },
473
502
 
474
503
  explain_query: {
@@ -753,6 +782,25 @@ module Parse
753
782
  },
754
783
  required: ["class_name"],
755
784
  },
785
+ output_schema: {
786
+ type: "object",
787
+ properties: {
788
+ class_name: { type: "string" },
789
+ # `format` is one of csv|markdown|table; same shape as the
790
+ # input enum.
791
+ format: { type: "string", enum: %w[csv markdown table] },
792
+ headers: { type: "array", items: { type: "string" } },
793
+ row_count: { type: "integer", minimum: 0 },
794
+ # The serialized output is the formatted CSV / Markdown /
795
+ # text-table string itself — clients render it as-is.
796
+ output: { type: "string" },
797
+ truncated: { type: "boolean" },
798
+ available_rows: { type: "integer", minimum: 0 },
799
+ row_cap: { type: "integer", minimum: 1 },
800
+ hint: { type: "string" },
801
+ },
802
+ required: %w[class_name format headers row_count output],
803
+ },
756
804
  },
757
805
 
758
806
  atlas_text_search: {
@@ -799,6 +847,50 @@ module Parse
799
847
  },
800
848
  required: %w[class_name query],
801
849
  },
850
+ output_schema: {
851
+ type: "object",
852
+ properties: {
853
+ class_name: { type: "string" },
854
+ count: { type: "integer", minimum: 0 },
855
+ # Each row is a Parse object projected through the class's
856
+ # agent_fields allowlist, with an Atlas-supplied `score`
857
+ # numeric and an optional `highlights` array when the
858
+ # caller passes highlight_field:. The row shape is class-
859
+ # dependent so additionalProperties is open.
860
+ results: {
861
+ type: "array",
862
+ items: {
863
+ type: "object",
864
+ properties: {
865
+ score: { type: "number" },
866
+ highlights: {
867
+ type: "array",
868
+ items: {
869
+ type: "object",
870
+ properties: {
871
+ path: { type: "string" },
872
+ texts: {
873
+ type: "array",
874
+ items: {
875
+ type: "object",
876
+ properties: {
877
+ value: { type: "string" },
878
+ type: { type: "string", description: "'hit' or 'text' per Atlas spec." },
879
+ },
880
+ required: %w[value],
881
+ },
882
+ },
883
+ },
884
+ required: %w[path],
885
+ },
886
+ },
887
+ },
888
+ additionalProperties: true,
889
+ },
890
+ },
891
+ },
892
+ required: %w[class_name count results],
893
+ },
802
894
  },
803
895
 
804
896
  atlas_autocomplete: {
@@ -838,6 +930,25 @@ module Parse
838
930
  },
839
931
  required: %w[class_name query field],
840
932
  },
933
+ output_schema: {
934
+ type: "object",
935
+ properties: {
936
+ class_name: { type: "string" },
937
+ field: { type: "string" },
938
+ # `suggestions` is the list of distinct field values that
939
+ # matched the autocomplete query (deduped, ordered by Atlas
940
+ # ranking). Strings only — autocomplete operates on text.
941
+ suggestions: { type: "array", items: { type: "string" } },
942
+ count: { type: "integer", minimum: 0 },
943
+ # Full matching Parse objects, projected through the class
944
+ # agent_fields allowlist.
945
+ results: {
946
+ type: "array",
947
+ items: { type: "object", additionalProperties: true },
948
+ },
949
+ },
950
+ required: %w[class_name field suggestions count results],
951
+ },
841
952
  },
842
953
 
843
954
  atlas_faceted_search: {
@@ -876,6 +987,40 @@ module Parse
876
987
  },
877
988
  required: %w[class_name facets],
878
989
  },
990
+ output_schema: {
991
+ type: "object",
992
+ properties: {
993
+ class_name: { type: "string" },
994
+ # $searchMeta lower-bound count. May be approximate for very
995
+ # large corpora — Atlas documents this; downstream clients
996
+ # should treat it as informative, not a precise total.
997
+ total_count: { type: "integer", minimum: 0 },
998
+ # Facets is a Map<facet_name, { buckets: [{_id, count}] }>.
999
+ # Bucket _id is heterogeneous (String for string facets,
1000
+ # Number/Date for numeric/date facets), so additionalProperties:true
1001
+ # on the bucket entry keeps the contract honest without
1002
+ # bloating the schema with per-type variants.
1003
+ facets: {
1004
+ type: "object",
1005
+ additionalProperties: {
1006
+ type: "object",
1007
+ properties: {
1008
+ buckets: {
1009
+ type: "array",
1010
+ items: { type: "object", additionalProperties: true },
1011
+ },
1012
+ },
1013
+ required: %w[buckets],
1014
+ },
1015
+ },
1016
+ count: { type: "integer", minimum: 0 },
1017
+ results: {
1018
+ type: "array",
1019
+ items: { type: "object", additionalProperties: true },
1020
+ },
1021
+ },
1022
+ required: %w[class_name total_count facets count results],
1023
+ },
879
1024
  },
880
1025
  }.freeze
881
1026
 
@@ -3474,7 +3619,14 @@ module Parse
3474
3619
  class_name: class_name,
3475
3620
  pipeline_stages: pipeline.size,
3476
3621
  result_count: results.size,
3477
- route: use_mongo_direct ? :mongo_direct : :parse_server,
3622
+ # Coerce to String here so the value lands in
3623
+ # `structuredContent` as a String (matching the
3624
+ # advertised output_schema `type: "string"`). Without the
3625
+ # `.to_s`, MCP clients validating structuredContent see a
3626
+ # Ruby Symbol pre-serialization and fail the type check;
3627
+ # downstream JSON serialization would convert it but the
3628
+ # client-side validator runs before that.
3629
+ route: (use_mongo_direct ? :mongo_direct : :parse_server).to_s,
3478
3630
  results: results,
3479
3631
  }
3480
3632
  result[:pointer_classes] = pointer_map if pointer_map.any?
@@ -51,6 +51,21 @@ module Parse
51
51
  @pool.with { |store| store.store(key, value, options) }
52
52
  end
53
53
 
54
+ # Atomic SETNX-style write. Required by `Parse::CreateLock` to acquire
55
+ # cross-process locks against Redis-backed stores. Forwards to the
56
+ # underlying Moneta store's `#create`, which returns `true` only if
57
+ # the key was absent and is now set.
58
+ def create(key, value, options = {})
59
+ @pool.with { |store| store.create(key, value, options) }
60
+ end
61
+
62
+ # Atomic counter increment. Forwarded for parity with Moneta so
63
+ # callers expecting the full Moneta surface (counters, rate limits)
64
+ # work transparently through the pool.
65
+ def increment(key, amount = 1, options = {})
66
+ @pool.with { |store| store.increment(key, amount, options) }
67
+ end
68
+
54
69
  # Clear the underlying backend. Pooled Moneta stores all point at the
55
70
  # same Redis DB, so a single checkout suffices — issuing `clear` on
56
71
  # one connection flushes the DB for every connection.