@checkstack/anomaly-backend 0.2.1 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,173 @@
1
1
  # @checkstack/anomaly-backend
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [2a749d3]
8
+ - @checkstack/healthcheck-backend@1.0.1
9
+
10
+ ## 1.0.0
11
+
12
+ ### Major Changes
13
+
14
+ - 32d52c6: feat: notification target pattern + per-spec subscriptions
15
+
16
+ Replaces the all-or-nothing catalog system/group notification model with a
17
+ platform-level target pattern. Each notification-emitting plugin declares
18
+ _subscription specs_ against typed _target_ objects exported from the
19
+ target's owning plugin (catalog ships `catalogSystemTarget` and
20
+ `catalogGroupTarget`). Notification-backend handles every per-resource
21
+ group lifecycle, parent-edge inheritance, and legacy-subscription seeding
22
+ — plugins never author groupId helpers, lifecycle hooks, or migration
23
+ code again.
24
+
25
+ **Plugin-author surface area is now ~12 lines per emitter:**
26
+
27
+ ```ts
28
+ // <plugin>-common
29
+ const { defineSubscription } = createSubscriptionFactory(pluginMetadata);
30
+ export const fooSystemSubscription = defineSubscription({
31
+ localId: "system",
32
+ target: catalogSystemTarget,
33
+ display: { title: "Foo Alerts", description: "...", iconName: "Bell" },
34
+ });
35
+
36
+ // <plugin>-backend register()
37
+ env.registerSubscriptionSpecs([fooSystemSubscription]);
38
+ // ^ feeds the plugin loader's dependency sorter — each spec's
39
+ // target.ownerPlugin becomes an implicit init-order dep, so this
40
+ // plugin automatically waits for catalog (the target owner) to
41
+ // finish init + afterPluginsReady before its own runs.
42
+
43
+ // <plugin>-backend afterPluginsReady
44
+ await notificationClient.registerSubscriptionSpec(
45
+ specToRegistration(fooSystemSubscription)
46
+ );
47
+ // dispatch
48
+ await notificationClient.notifyForSubscription({
49
+ specId: fooSystemSubscription.specId,
50
+ resourceKeys: [systemId],
51
+ title,
52
+ body,
53
+ importance,
54
+ action,
55
+ collapseKey,
56
+ subjects,
57
+ });
58
+
59
+ // <plugin>-frontend
60
+ createNotificationSubscriptionExtension({ spec: fooSystemSubscription });
61
+ ```
62
+
63
+ **Migrated plugins**: anomaly, incident, maintenance, healthcheck,
64
+ dependency. Each lost its bespoke `notification-groups.ts`,
65
+ `bootstrap*NotificationGroups`, `ensure*Group`, and inheritance walk —
66
+ all of that is now centralized in notification-backend's
67
+ `subscription-engine`.
68
+
69
+ **Plugin loader change** (`@checkstack/backend-api`,
70
+ `@checkstack/backend`): the register-time API gains
71
+ `env.registerSubscriptionSpecs([...specs])`. The dependency sorter
72
+ walks `spec.target.ownerPlugin` for every declared spec and adds the
73
+ target owner as an init-order dependency of the emitting plugin. This
74
+ guarantees that catalog (the owner of the platform's `system` and
75
+ `group` targets) completes init + afterPluginsReady before any
76
+ emitting plugin tries to register its specs against the notification
77
+ service — no string-prefix heuristics, no manual `dependsOnPlugins`
78
+ list, no stub rows. Plugins that fail to declare their specs at
79
+ register time get a clear `Target type X is not registered. Did the
80
+ emitting plugin declare this spec via env.registerSubscriptionSpecs?`
81
+ error from the dispatcher.
82
+
83
+ **Removed** (no backwards compat):
84
+
85
+ - `catalogClient.notifySystemSubscribers` and
86
+ `catalogClient.notifyManySystemSubscribers`
87
+ - `notificationClient.notifyUsers` and `notificationClient.notifyGroups`
88
+ as direct dispatch primitives — replaced by spec-bound
89
+ `notifyForSubscription`
90
+ - catalog's `bootstrapNotificationGroups` (replaced by
91
+ `bootstrapNotificationTargets`)
92
+
93
+ **Enforcement**: the dispatcher rejects calls referencing unregistered
94
+ specIds, specs owned by other plugins, or resourceKeys that haven't been
95
+ pushed via `upsertNotificationResource`. Display metadata for any
96
+ groupId is recoverable via the spec registry, so audit lists render
97
+ correct labels even when an emitter's frontend isn't loaded.
98
+
99
+ **Per-field anomaly mute** keeps working — it now lives inside the
100
+ generic SubscriptionRow's optional `SubControls` panel
101
+ (`AnomalyFieldMuteList`), exposed through the catalog system detail
102
+ page's notifications card.
103
+
104
+ The catalog system detail page renders a "Notifications" card hosting
105
+ `SystemNotificationSubscriptionsSlot`. The matching group surface is
106
+ not yet rendered — group-level subscriptions are wired end-to-end on
107
+ the backend; a follow-up will add the host UI.
108
+
109
+ **Migration of existing subscribers**: target types declare a
110
+ `legacyGroupIdTemplate`; on first registration of each spec,
111
+ notification-backend reads subscribers from the legacy
112
+ `catalog.system.<id>` / `catalog.group.<id>` groups and seeds the new
113
+ spec groups exactly once per (spec × resource) pair, tracked in
114
+ `subscription_migrations`. Anomaly stays opt-in (its target also
115
+ declares the template, but the user-explicit nature of the original
116
+ opt-in flow means the seeding produces the same set of subscribers
117
+ they already had).
118
+
119
+ ### Minor Changes
120
+
121
+ - 32d52c6: feat(anomaly): per-system and per-field notification mute
122
+
123
+ Anomaly notifications now flow through their own subscription group
124
+ (`anomaly.system.<systemId>`) instead of the shared catalog system group, so
125
+ users can opt out of anomaly noise without losing incident or healthcheck
126
+ alerts for the same system. On first deploy, existing subscribers of each
127
+ `catalog.system.<id>` group are seeded onto the new anomaly group so no one
128
+ silently stops getting alerts.
129
+
130
+ A new mute table (`anomaly_notification_mutes`) backs two granularities:
131
+
132
+ - **Per-field**: silence a single noisy metric on one system.
133
+ - **Per-system**: silence every anomaly for one system in one click.
134
+
135
+ The system anomaly widget now exposes a bell icon on each anomaly row plus a
136
+ `Mute all` toggle in the card header. Mutes are user-scoped and persist
137
+ across sessions.
138
+
139
+ Catalog gains a `systemCreated` hook so anomaly (and any future plugin) can
140
+ provision per-system state on creation rather than waiting for a restart.
141
+ The notification service gains a `bulkSubscribe` service-RPC used by the
142
+ one-time migration described above.
143
+
144
+ ### Patch Changes
145
+
146
+ - 32d52c6: Bulk notifications affecting multiple systems and collapse lifecycle events into a single card.
147
+
148
+ Notifications now carry an optional `subjects` array (the entities they affect) and an optional `collapseKey` (so related notifications collapse into one row per recipient). Incidents, maintenances, anomalies, healthchecks, and dependency-impact events route through these new fields, so an incident affecting three systems produces one in-app notification + one external send per subscriber instead of three. Lifecycle updates for the same entity (created → updated → resolved) also collapse, with an expandable "+N updates" timeline.
149
+
150
+ Subject kinds are namespaced as `<pluginId>.<localKind>` and built via type-safe helpers exported from each domain's common package (`createSystemSubject`, `incidentCollapseKey`, etc.). The frontend kind registry (`registerSubjectKind`) lets plugins bind icon + label for their kinds; unknown kinds fall back to a generic chip.
151
+
152
+ All notification strategies (SMTP, Slack, Discord, Teams, Telegram, Pushover, Gotify, Webex, Backstage) render the affected subjects natively in their format (HTML cards, Slack blocks, Discord embed fields, adaptive cards, markdown lists, etc.).
153
+
154
+ - 32d52c6: Add missing workspace/runtime deps that were only resolving locally via stale `node_modules` symlinks: `@checkstack/signal-common` in `anomaly-backend` and `@orpc/contract` in `frontend-api`. Both were imported as `import type` and went unflagged by the `no-extraneous-runtime-deps` rule, but failed `tsc` on clean CI installs.
155
+ - Updated dependencies [32d52c6]
156
+ - Updated dependencies [32d52c6]
157
+ - Updated dependencies [32d52c6]
158
+ - Updated dependencies [32d52c6]
159
+ - Updated dependencies [32d52c6]
160
+ - @checkstack/anomaly-common@1.0.0
161
+ - @checkstack/notification-common@1.0.0
162
+ - @checkstack/catalog-backend@1.0.0
163
+ - @checkstack/catalog-common@2.0.0
164
+ - @checkstack/healthcheck-common@1.0.0
165
+ - @checkstack/healthcheck-backend@1.0.0
166
+ - @checkstack/backend-api@0.14.0
167
+ - @checkstack/cache-api@0.2.2
168
+ - @checkstack/queue-api@0.2.16
169
+ - @checkstack/cache-utils@0.2.2
170
+
3
171
  ## 0.2.1
4
172
 
5
173
  ### Patch Changes
@@ -0,0 +1,10 @@
1
+ CREATE TABLE "anomaly_notification_mutes" (
2
+ "user_id" text NOT NULL,
3
+ "system_id" text NOT NULL,
4
+ "field_path" text NOT NULL,
5
+ "muted_at" timestamp DEFAULT now() NOT NULL,
6
+ CONSTRAINT "anomaly_notification_mutes_user_id_system_id_field_path_pk" PRIMARY KEY("user_id","system_id","field_path")
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE INDEX "anomaly_notification_mutes_user_idx" ON "anomaly_notification_mutes" USING btree ("user_id");--> statement-breakpoint
10
+ CREATE INDEX "anomaly_notification_mutes_system_idx" ON "anomaly_notification_mutes" USING btree ("system_id");
@@ -0,0 +1,401 @@
1
+ {
2
+ "id": "d2a17ab6-efcd-4936-a870-ee045c74e10e",
3
+ "prevId": "ba98dce1-b88e-41d5-aa8d-730ba5e94805",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.anomalies": {
8
+ "name": "anomalies",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "system_id": {
19
+ "name": "system_id",
20
+ "type": "text",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "configuration_id": {
25
+ "name": "configuration_id",
26
+ "type": "uuid",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "field_path": {
31
+ "name": "field_path",
32
+ "type": "text",
33
+ "primaryKey": false,
34
+ "notNull": true
35
+ },
36
+ "kind": {
37
+ "name": "kind",
38
+ "type": "anomaly_kind",
39
+ "typeSchema": "public",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": "'spike'"
43
+ },
44
+ "state": {
45
+ "name": "state",
46
+ "type": "anomaly_state",
47
+ "typeSchema": "public",
48
+ "primaryKey": false,
49
+ "notNull": true
50
+ },
51
+ "direction": {
52
+ "name": "direction",
53
+ "type": "anomaly_direction",
54
+ "typeSchema": "public",
55
+ "primaryKey": false,
56
+ "notNull": true
57
+ },
58
+ "baseline_value": {
59
+ "name": "baseline_value",
60
+ "type": "double precision",
61
+ "primaryKey": false,
62
+ "notNull": false
63
+ },
64
+ "baseline_std_dev": {
65
+ "name": "baseline_std_dev",
66
+ "type": "double precision",
67
+ "primaryKey": false,
68
+ "notNull": false
69
+ },
70
+ "observed_value": {
71
+ "name": "observed_value",
72
+ "type": "text",
73
+ "primaryKey": false,
74
+ "notNull": true
75
+ },
76
+ "deviation": {
77
+ "name": "deviation",
78
+ "type": "double precision",
79
+ "primaryKey": false,
80
+ "notNull": true
81
+ },
82
+ "suspicious_run_count": {
83
+ "name": "suspicious_run_count",
84
+ "type": "integer",
85
+ "primaryKey": false,
86
+ "notNull": true,
87
+ "default": 0
88
+ },
89
+ "confirmation_threshold": {
90
+ "name": "confirmation_threshold",
91
+ "type": "integer",
92
+ "primaryKey": false,
93
+ "notNull": true
94
+ },
95
+ "started_at": {
96
+ "name": "started_at",
97
+ "type": "timestamp",
98
+ "primaryKey": false,
99
+ "notNull": true,
100
+ "default": "now()"
101
+ },
102
+ "confirmed_at": {
103
+ "name": "confirmed_at",
104
+ "type": "timestamp",
105
+ "primaryKey": false,
106
+ "notNull": false
107
+ },
108
+ "recovered_at": {
109
+ "name": "recovered_at",
110
+ "type": "timestamp",
111
+ "primaryKey": false,
112
+ "notNull": false
113
+ },
114
+ "metadata": {
115
+ "name": "metadata",
116
+ "type": "jsonb",
117
+ "primaryKey": false,
118
+ "notNull": false
119
+ }
120
+ },
121
+ "indexes": {},
122
+ "foreignKeys": {},
123
+ "compositePrimaryKeys": {},
124
+ "uniqueConstraints": {},
125
+ "policies": {},
126
+ "checkConstraints": {},
127
+ "isRLSEnabled": false
128
+ },
129
+ "public.anomaly_assignments": {
130
+ "name": "anomaly_assignments",
131
+ "schema": "",
132
+ "columns": {
133
+ "system_id": {
134
+ "name": "system_id",
135
+ "type": "text",
136
+ "primaryKey": false,
137
+ "notNull": true
138
+ },
139
+ "configuration_id": {
140
+ "name": "configuration_id",
141
+ "type": "uuid",
142
+ "primaryKey": false,
143
+ "notNull": true
144
+ },
145
+ "config": {
146
+ "name": "config",
147
+ "type": "jsonb",
148
+ "primaryKey": false,
149
+ "notNull": true
150
+ }
151
+ },
152
+ "indexes": {},
153
+ "foreignKeys": {},
154
+ "compositePrimaryKeys": {},
155
+ "uniqueConstraints": {
156
+ "anomaly_assignments_pk": {
157
+ "name": "anomaly_assignments_pk",
158
+ "nullsNotDistinct": false,
159
+ "columns": [
160
+ "system_id",
161
+ "configuration_id"
162
+ ]
163
+ }
164
+ },
165
+ "policies": {},
166
+ "checkConstraints": {},
167
+ "isRLSEnabled": false
168
+ },
169
+ "public.anomaly_baselines": {
170
+ "name": "anomaly_baselines",
171
+ "schema": "",
172
+ "columns": {
173
+ "id": {
174
+ "name": "id",
175
+ "type": "uuid",
176
+ "primaryKey": true,
177
+ "notNull": true,
178
+ "default": "gen_random_uuid()"
179
+ },
180
+ "system_id": {
181
+ "name": "system_id",
182
+ "type": "text",
183
+ "primaryKey": false,
184
+ "notNull": true
185
+ },
186
+ "configuration_id": {
187
+ "name": "configuration_id",
188
+ "type": "uuid",
189
+ "primaryKey": false,
190
+ "notNull": true
191
+ },
192
+ "field_path": {
193
+ "name": "field_path",
194
+ "type": "text",
195
+ "primaryKey": false,
196
+ "notNull": true
197
+ },
198
+ "mean": {
199
+ "name": "mean",
200
+ "type": "double precision",
201
+ "primaryKey": false,
202
+ "notNull": true
203
+ },
204
+ "std_dev": {
205
+ "name": "std_dev",
206
+ "type": "double precision",
207
+ "primaryKey": false,
208
+ "notNull": true
209
+ },
210
+ "trend_slope": {
211
+ "name": "trend_slope",
212
+ "type": "double precision",
213
+ "primaryKey": false,
214
+ "notNull": true
215
+ },
216
+ "sample_count": {
217
+ "name": "sample_count",
218
+ "type": "integer",
219
+ "primaryKey": false,
220
+ "notNull": true
221
+ },
222
+ "computed_at": {
223
+ "name": "computed_at",
224
+ "type": "timestamp",
225
+ "primaryKey": false,
226
+ "notNull": true
227
+ },
228
+ "dominant_value": {
229
+ "name": "dominant_value",
230
+ "type": "text",
231
+ "primaryKey": false,
232
+ "notNull": false
233
+ },
234
+ "dominant_ratio": {
235
+ "name": "dominant_ratio",
236
+ "type": "double precision",
237
+ "primaryKey": false,
238
+ "notNull": false
239
+ }
240
+ },
241
+ "indexes": {},
242
+ "foreignKeys": {},
243
+ "compositePrimaryKeys": {},
244
+ "uniqueConstraints": {
245
+ "anomaly_baselines_unique_path": {
246
+ "name": "anomaly_baselines_unique_path",
247
+ "nullsNotDistinct": false,
248
+ "columns": [
249
+ "system_id",
250
+ "configuration_id",
251
+ "field_path"
252
+ ]
253
+ }
254
+ },
255
+ "policies": {},
256
+ "checkConstraints": {},
257
+ "isRLSEnabled": false
258
+ },
259
+ "public.anomaly_configurations": {
260
+ "name": "anomaly_configurations",
261
+ "schema": "",
262
+ "columns": {
263
+ "configuration_id": {
264
+ "name": "configuration_id",
265
+ "type": "uuid",
266
+ "primaryKey": true,
267
+ "notNull": true
268
+ },
269
+ "config": {
270
+ "name": "config",
271
+ "type": "jsonb",
272
+ "primaryKey": false,
273
+ "notNull": true
274
+ }
275
+ },
276
+ "indexes": {},
277
+ "foreignKeys": {},
278
+ "compositePrimaryKeys": {},
279
+ "uniqueConstraints": {},
280
+ "policies": {},
281
+ "checkConstraints": {},
282
+ "isRLSEnabled": false
283
+ },
284
+ "public.anomaly_notification_mutes": {
285
+ "name": "anomaly_notification_mutes",
286
+ "schema": "",
287
+ "columns": {
288
+ "user_id": {
289
+ "name": "user_id",
290
+ "type": "text",
291
+ "primaryKey": false,
292
+ "notNull": true
293
+ },
294
+ "system_id": {
295
+ "name": "system_id",
296
+ "type": "text",
297
+ "primaryKey": false,
298
+ "notNull": true
299
+ },
300
+ "field_path": {
301
+ "name": "field_path",
302
+ "type": "text",
303
+ "primaryKey": false,
304
+ "notNull": true
305
+ },
306
+ "muted_at": {
307
+ "name": "muted_at",
308
+ "type": "timestamp",
309
+ "primaryKey": false,
310
+ "notNull": true,
311
+ "default": "now()"
312
+ }
313
+ },
314
+ "indexes": {
315
+ "anomaly_notification_mutes_user_idx": {
316
+ "name": "anomaly_notification_mutes_user_idx",
317
+ "columns": [
318
+ {
319
+ "expression": "user_id",
320
+ "isExpression": false,
321
+ "asc": true,
322
+ "nulls": "last"
323
+ }
324
+ ],
325
+ "isUnique": false,
326
+ "concurrently": false,
327
+ "method": "btree",
328
+ "with": {}
329
+ },
330
+ "anomaly_notification_mutes_system_idx": {
331
+ "name": "anomaly_notification_mutes_system_idx",
332
+ "columns": [
333
+ {
334
+ "expression": "system_id",
335
+ "isExpression": false,
336
+ "asc": true,
337
+ "nulls": "last"
338
+ }
339
+ ],
340
+ "isUnique": false,
341
+ "concurrently": false,
342
+ "method": "btree",
343
+ "with": {}
344
+ }
345
+ },
346
+ "foreignKeys": {},
347
+ "compositePrimaryKeys": {
348
+ "anomaly_notification_mutes_user_id_system_id_field_path_pk": {
349
+ "name": "anomaly_notification_mutes_user_id_system_id_field_path_pk",
350
+ "columns": [
351
+ "user_id",
352
+ "system_id",
353
+ "field_path"
354
+ ]
355
+ }
356
+ },
357
+ "uniqueConstraints": {},
358
+ "policies": {},
359
+ "checkConstraints": {},
360
+ "isRLSEnabled": false
361
+ }
362
+ },
363
+ "enums": {
364
+ "public.anomaly_direction": {
365
+ "name": "anomaly_direction",
366
+ "schema": "public",
367
+ "values": [
368
+ "above",
369
+ "below",
370
+ "changed"
371
+ ]
372
+ },
373
+ "public.anomaly_kind": {
374
+ "name": "anomaly_kind",
375
+ "schema": "public",
376
+ "values": [
377
+ "spike",
378
+ "drift"
379
+ ]
380
+ },
381
+ "public.anomaly_state": {
382
+ "name": "anomaly_state",
383
+ "schema": "public",
384
+ "values": [
385
+ "suspicious",
386
+ "anomaly",
387
+ "recovered"
388
+ ]
389
+ }
390
+ },
391
+ "schemas": {},
392
+ "sequences": {},
393
+ "roles": {},
394
+ "policies": {},
395
+ "views": {},
396
+ "_meta": {
397
+ "columns": {},
398
+ "schemas": {},
399
+ "tables": {}
400
+ }
401
+ }
@@ -29,6 +29,13 @@
29
29
  "when": 1777459740891,
30
30
  "tag": "0003_easy_maginty",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1777549186811,
37
+ "tag": "0004_gray_trauma",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/anomaly-backend",
3
- "version": "0.2.1",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -13,15 +13,18 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/backend-api": "0.13.0",
16
+ "@checkstack/backend-api": "0.13.1",
17
17
  "@checkstack/common": "0.7.0",
18
- "@checkstack/anomaly-common": "0.2.0",
19
- "@checkstack/healthcheck-common": "0.12.0",
20
- "@checkstack/queue-api": "0.2.14",
21
- "@checkstack/cache-api": "0.2.0",
22
- "@checkstack/cache-utils": "0.2.0",
23
- "@checkstack/healthcheck-backend": "0.18.0",
24
- "@checkstack/catalog-common": "1.5.2",
18
+ "@checkstack/anomaly-common": "0.3.0",
19
+ "@checkstack/signal-common": "0.2.0",
20
+ "@checkstack/healthcheck-common": "0.13.0",
21
+ "@checkstack/queue-api": "0.2.15",
22
+ "@checkstack/cache-api": "0.2.1",
23
+ "@checkstack/cache-utils": "0.2.1",
24
+ "@checkstack/healthcheck-backend": "0.18.1",
25
+ "@checkstack/catalog-backend": "0.7.1",
26
+ "@checkstack/catalog-common": "1.5.3",
27
+ "@checkstack/notification-common": "0.3.0",
25
28
  "drizzle-orm": "^0.45.0",
26
29
  "hono": "^4.12.14",
27
30
  "zod": "^4.2.1",
@@ -30,10 +33,11 @@
30
33
  "devDependencies": {
31
34
  "@checkstack/drizzle-helper": "0.0.4",
32
35
  "@checkstack/scripts": "0.1.2",
33
- "@checkstack/test-utils-backend": "0.1.20",
36
+ "@checkstack/test-utils-backend": "0.1.21",
34
37
  "@checkstack/tsconfig": "0.0.5",
35
38
  "@types/bun": "^1.0.0",
36
39
  "date-fns": "^4.1.0",
40
+ "drizzle-kit": "^0.31.10",
37
41
  "typescript": "^5.0.0"
38
42
  }
39
43
  }