@ontrails/warden 1.0.0-beta.39 → 1.0.0-beta.41

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 (134) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +5 -5
  3. package/package.json +10 -12
  4. package/src/cli.ts +49 -44
  5. package/src/command.ts +2 -2
  6. package/src/drift.ts +4 -3
  7. package/src/index.ts +3 -2
  8. package/src/project-context.ts +3 -3
  9. package/src/resolve.ts +2 -2
  10. package/src/rules/captured-kernel.ts +375 -0
  11. package/src/rules/circular-refs.ts +12 -16
  12. package/src/rules/cli-command-route-coherence.ts +1 -1
  13. package/src/rules/composes-declarations.ts +15 -13
  14. package/src/rules/context-no-surface-types.ts +4 -3
  15. package/src/rules/dead-internal-trail.ts +3 -3
  16. package/src/rules/dead-public-trail.ts +3 -3
  17. package/src/rules/draft-file-marking.ts +4 -9
  18. package/src/rules/draft-visible-debt.ts +3 -8
  19. package/src/rules/duplicate-public-contract.ts +50 -5
  20. package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
  21. package/src/rules/entity-ids.ts +15 -0
  22. package/src/rules/error-mapping-completeness.ts +2 -2
  23. package/src/rules/example-valid.ts +84 -84
  24. package/src/rules/fires-declarations.ts +27 -22
  25. package/src/rules/governed-symbol-residue.ts +339 -32
  26. package/src/rules/implementation-returns-result.ts +8 -8
  27. package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
  28. package/src/rules/incomplete-crud.ts +42 -40
  29. package/src/rules/index.ts +9 -6
  30. package/src/rules/intent-propagation.ts +3 -5
  31. package/src/rules/layer-field-name-drift.ts +2 -2
  32. package/src/rules/library-projection-coherence.ts +2 -2
  33. package/src/rules/metadata.ts +37 -15
  34. package/src/rules/missing-reconcile.ts +3 -4
  35. package/src/rules/missing-visibility.ts +2 -1
  36. package/src/rules/no-destructured-compose.ts +14 -12
  37. package/src/rules/no-direct-implementation-call.ts +12 -7
  38. package/src/rules/no-legacy-cli-alias-export.ts +2 -2
  39. package/src/rules/no-native-error-result.ts +2 -2
  40. package/src/rules/no-redundant-result-error-wrap.ts +10 -10
  41. package/src/rules/no-sync-result-assumption.ts +84 -78
  42. package/src/rules/no-throw-in-detour-recover.ts +2 -2
  43. package/src/rules/no-throw-in-implementation.ts +13 -7
  44. package/src/rules/no-top-level-surface.ts +2 -2
  45. package/src/rules/on-references-exist.ts +3 -3
  46. package/src/rules/orphaned-signal.ts +4 -5
  47. package/src/rules/owner-projection-parity.ts +2 -2
  48. package/src/rules/public-export-example-coverage.ts +10 -4
  49. package/src/rules/public-internal-deep-imports.ts +2 -3
  50. package/src/rules/read-intent-fires.ts +4 -3
  51. package/src/rules/reference-exists.ts +20 -21
  52. package/src/rules/registry-names.ts +6 -4
  53. package/src/rules/resolved-import-boundary.ts +1 -1
  54. package/src/rules/resource-declarations.ts +21 -17
  55. package/src/rules/resource-exists.ts +4 -2
  56. package/src/rules/resource-id-grammar.ts +2 -2
  57. package/src/rules/resource-mock-coverage.ts +2 -2
  58. package/src/rules/retired-vocabulary.ts +242 -5
  59. package/src/rules/source/composition.ts +165 -0
  60. package/src/rules/source/drafts.ts +164 -0
  61. package/src/rules/source/entities.ts +618 -0
  62. package/src/rules/source/pragmas.ts +45 -0
  63. package/src/rules/source/resources.ts +64 -0
  64. package/src/rules/source/signals.ts +397 -0
  65. package/src/rules/source/stores.ts +310 -0
  66. package/src/rules/static-resource-accessor-preference.ts +15 -11
  67. package/src/rules/surface-overlay-coherence.ts +1 -1
  68. package/src/rules/surface-trailhead-coherence.ts +2 -2
  69. package/src/rules/trail-fork-coaching.ts +27 -18
  70. package/src/rules/trail-versioning-source.ts +16 -12
  71. package/src/rules/trail-versioning-topo.ts +2 -2
  72. package/src/rules/trailhead-override-divergence.ts +2 -2
  73. package/src/rules/types.ts +7 -7
  74. package/src/rules/unmaterialized-activation-source.ts +1 -0
  75. package/src/rules/unreachable-detour-shadowing.ts +2 -2
  76. package/src/rules/valid-describe-refs.ts +2 -2
  77. package/src/rules/warden-export-symmetry.ts +3 -3
  78. package/src/rules/warden-rules-use-ast.ts +12 -17
  79. package/src/trails/activation-orphan.trail.ts +5 -5
  80. package/src/trails/captured-kernel.trail.ts +108 -0
  81. package/src/trails/circular-refs.trail.ts +6 -6
  82. package/src/trails/cli-command-route-coherence.trail.ts +2 -2
  83. package/src/trails/composes-declarations.trail.ts +1 -1
  84. package/src/trails/context-no-surface-types.trail.ts +1 -1
  85. package/src/trails/dead-internal-trail.trail.ts +2 -2
  86. package/src/trails/dead-public-trail.trail.ts +1 -1
  87. package/src/trails/deprecation-without-guidance.trail.ts +1 -1
  88. package/src/trails/duplicate-public-contract.trail.ts +3 -3
  89. package/src/trails/entity-exists.trail.ts +21 -0
  90. package/src/trails/example-valid.trail.ts +3 -3
  91. package/src/trails/fires-declarations.trail.ts +1 -1
  92. package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
  93. package/src/trails/implementation-returns-result.trail.ts +1 -1
  94. package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
  95. package/src/trails/index.ts +3 -2
  96. package/src/trails/intent-propagation.trail.ts +2 -2
  97. package/src/trails/library-projection-coherence.trail.ts +2 -2
  98. package/src/trails/marker-schema-unsupported.trail.ts +1 -1
  99. package/src/trails/missing-visibility.trail.ts +1 -1
  100. package/src/trails/no-destructured-compose.trail.ts +5 -5
  101. package/src/trails/no-direct-implementation-call.trail.ts +1 -1
  102. package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
  103. package/src/trails/no-sync-result-assumption.trail.ts +2 -2
  104. package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
  105. package/src/trails/no-throw-in-implementation.trail.ts +1 -1
  106. package/src/trails/on-references-exist.trail.ts +1 -1
  107. package/src/trails/pending-force.trail.ts +1 -1
  108. package/src/trails/permit-governance.trail.ts +2 -2
  109. package/src/trails/prefer-schema-inference.trail.ts +1 -1
  110. package/src/trails/public-output-schema.trail.ts +2 -2
  111. package/src/trails/public-union-output-discriminants.trail.ts +1 -1
  112. package/src/trails/read-intent-fires.trail.ts +1 -1
  113. package/src/trails/reference-exists.trail.ts +5 -5
  114. package/src/trails/resource-declarations.trail.ts +1 -1
  115. package/src/trails/resource-exists.trail.ts +1 -1
  116. package/src/trails/run.ts +4 -6
  117. package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
  118. package/src/trails/schema.ts +9 -9
  119. package/src/trails/signal-graph-coaching.trail.ts +2 -2
  120. package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
  121. package/src/trails/surface-overlay-coherence.trail.ts +1 -1
  122. package/src/trails/trail-fork-coaching.trail.ts +1 -1
  123. package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
  124. package/src/trails/valid-detour-contract.trail.ts +1 -1
  125. package/src/trails/version-gap.trail.ts +1 -1
  126. package/src/trails/version-pinned-compose.trail.ts +1 -1
  127. package/src/trails/version-without-examples.trail.ts +1 -1
  128. package/src/trails/warden-rules-use-ast.trail.ts +2 -2
  129. package/src/trails/webhook-route-collision.trail.ts +2 -2
  130. package/src/trails/wrap-rule.ts +17 -17
  131. package/src/ast.ts +0 -137
  132. package/src/rules/ast.ts +0 -4143
  133. package/src/rules/contour-ids.ts +0 -15
  134. package/src/trails/contour-exists.trail.ts +0 -21
@@ -0,0 +1,64 @@
1
+ /** Warden-private resource declaration helpers. */
2
+
3
+ import {
4
+ extractBindingName,
5
+ extractFirstStringArg,
6
+ identifierName,
7
+ walk,
8
+ } from '@ontrails/source';
9
+ import type { AstNode } from '@ontrails/source';
10
+
11
+ const isResourceCall = (node: AstNode | undefined): boolean =>
12
+ !!node &&
13
+ node.type === 'CallExpression' &&
14
+ identifierName((node as unknown as { callee?: AstNode }).callee) ===
15
+ 'resource';
16
+
17
+ /** Collect `const foo = resource('id', ...)` bindings from a parsed file. */
18
+ export const collectNamedResourceIds = (
19
+ ast: AstNode
20
+ ): ReadonlyMap<string, string> => {
21
+ const ids = new Map<string, string>();
22
+
23
+ walk(ast, (node) => {
24
+ if (node.type !== 'VariableDeclarator') {
25
+ return;
26
+ }
27
+
28
+ const { id, init } = node as unknown as {
29
+ readonly id?: AstNode;
30
+ readonly init?: AstNode;
31
+ };
32
+ if (!isResourceCall(init)) {
33
+ return;
34
+ }
35
+
36
+ const name = extractBindingName(id);
37
+ const resourceId = init ? extractFirstStringArg(init) : null;
38
+ if (name && resourceId) {
39
+ ids.set(name, resourceId);
40
+ }
41
+ });
42
+
43
+ return ids;
44
+ };
45
+
46
+ /** Collect all inline `resource('id', ...)` definition IDs from a parsed file. */
47
+ export const collectResourceDefinitionIds = (
48
+ ast: AstNode
49
+ ): ReadonlySet<string> => {
50
+ const ids = new Set<string>();
51
+
52
+ walk(ast, (node) => {
53
+ if (!isResourceCall(node)) {
54
+ return;
55
+ }
56
+
57
+ const id = extractFirstStringArg(node);
58
+ if (id) {
59
+ ids.add(id);
60
+ }
61
+ });
62
+
63
+ return ids;
64
+ };
@@ -0,0 +1,397 @@
1
+ /** Warden-private signal declaration and on-target helpers. */
2
+
3
+ import {
4
+ buildFrameworkNamespaceContext,
5
+ collectScopeFrameBindings,
6
+ deriveConstString,
7
+ extractBindingName,
8
+ extractPlainTemplateLiteral,
9
+ extractStringLiteral,
10
+ extractTrailDefinition,
11
+ findConfigProperty,
12
+ findTrailDefinitions,
13
+ getPropertyName,
14
+ getStringValue,
15
+ identifierName,
16
+ isScopeFrameNode,
17
+ isStringLiteral,
18
+ walk,
19
+ } from '@ontrails/source';
20
+ import type { AstNode, FrameworkNamespaceContext } from '@ontrails/source';
21
+ import {
22
+ collectNamedStoreTableIds,
23
+ deriveStoreTableId,
24
+ getMemberExpression,
25
+ } from './stores.js';
26
+
27
+ export const collectSignalDefinitionIds = (
28
+ ast: AstNode
29
+ ): ReadonlySet<string> => {
30
+ const ids = new Set<string>();
31
+ for (const def of findTrailDefinitions(ast)) {
32
+ if (def.kind === 'signal') {
33
+ ids.add(def.id);
34
+ }
35
+ }
36
+ return ids;
37
+ };
38
+
39
+ const unwrapTopLevelDeclaration = (stmt: AstNode): AstNode => {
40
+ if (
41
+ stmt.type === 'ExportNamedDeclaration' ||
42
+ stmt.type === 'ExportDefaultDeclaration'
43
+ ) {
44
+ return (stmt as unknown as { declaration?: AstNode }).declaration ?? stmt;
45
+ }
46
+ return stmt;
47
+ };
48
+
49
+ const collectSignalIdsFromDeclaration = (
50
+ declaration: AstNode,
51
+ context: FrameworkNamespaceContext,
52
+ ids: Map<string, string>
53
+ ): void => {
54
+ const declarations =
55
+ (
56
+ unwrapTopLevelDeclaration(declaration) as unknown as {
57
+ declarations?: readonly AstNode[];
58
+ }
59
+ ).declarations ?? [];
60
+
61
+ for (const node of declarations) {
62
+ const { id, init } = node as unknown as {
63
+ readonly id?: AstNode;
64
+ readonly init?: AstNode;
65
+ };
66
+ if (!init) {
67
+ continue;
68
+ }
69
+
70
+ const def = extractTrailDefinition(init, context);
71
+ const name = extractBindingName(id);
72
+ if (def?.kind === 'signal' && name && !ids.has(name)) {
73
+ ids.set(name, def.id);
74
+ }
75
+ }
76
+ };
77
+
78
+ const collectStringIdsFromDeclaration = (
79
+ declaration: AstNode,
80
+ ids: Map<string, string>
81
+ ): void => {
82
+ const declarations =
83
+ (
84
+ unwrapTopLevelDeclaration(declaration) as unknown as {
85
+ declarations?: readonly AstNode[];
86
+ }
87
+ ).declarations ?? [];
88
+
89
+ for (const node of declarations) {
90
+ const { id, init } = node as unknown as {
91
+ readonly id?: AstNode;
92
+ readonly init?: AstNode;
93
+ };
94
+ if (!init) {
95
+ continue;
96
+ }
97
+
98
+ const name = extractBindingName(id);
99
+ const value =
100
+ extractStringLiteral(init) ?? extractPlainTemplateLiteral(init);
101
+ if (name && value !== null && !ids.has(name)) {
102
+ ids.set(name, value);
103
+ }
104
+ }
105
+ };
106
+
107
+ export type SignalIdentifierResolution =
108
+ | {
109
+ readonly id: string;
110
+ readonly kind: 'signal' | 'string';
111
+ }
112
+ | {
113
+ readonly kind: 'shadowed' | 'unbound';
114
+ };
115
+
116
+ export interface SignalIdentifierResolver {
117
+ readonly resolve: (reference: AstNode) => SignalIdentifierResolution;
118
+ }
119
+
120
+ interface SignalScopeFrame {
121
+ readonly bindings: ReadonlySet<string>;
122
+ readonly end: number;
123
+ readonly signals: ReadonlyMap<string, string>;
124
+ readonly start: number;
125
+ readonly strings: ReadonlyMap<string, string>;
126
+ }
127
+
128
+ const collectSignalFrameValues = (
129
+ node: AstNode,
130
+ context: FrameworkNamespaceContext
131
+ ): {
132
+ readonly signals: ReadonlyMap<string, string>;
133
+ readonly strings: ReadonlyMap<string, string>;
134
+ } => {
135
+ const signals = new Map<string, string>();
136
+ const strings = new Map<string, string>();
137
+
138
+ const collectDeclaration = (statement: AstNode): void => {
139
+ const declaration = unwrapTopLevelDeclaration(statement);
140
+ if (declaration.type !== 'VariableDeclaration') {
141
+ return;
142
+ }
143
+ collectSignalIdsFromDeclaration(declaration, context, signals);
144
+ collectStringIdsFromDeclaration(declaration, strings);
145
+ };
146
+
147
+ if (
148
+ node.type === 'Program' ||
149
+ node.type === 'BlockStatement' ||
150
+ node.type === 'FunctionBody'
151
+ ) {
152
+ const body = (node as unknown as { body?: readonly AstNode[] }).body ?? [];
153
+ for (const statement of body) {
154
+ collectDeclaration(statement);
155
+ }
156
+ }
157
+
158
+ if (node.type === 'ForStatement') {
159
+ const { init } = node as unknown as { init?: AstNode };
160
+ if (init) {
161
+ collectDeclaration(init);
162
+ }
163
+ }
164
+
165
+ if (node.type === 'SwitchStatement') {
166
+ const cases =
167
+ (node as unknown as { cases?: readonly AstNode[] }).cases ?? [];
168
+ for (const item of cases) {
169
+ const consequent =
170
+ (item as unknown as { consequent?: readonly AstNode[] }).consequent ??
171
+ [];
172
+ for (const statement of consequent) {
173
+ collectDeclaration(statement);
174
+ }
175
+ }
176
+ }
177
+
178
+ return { signals, strings };
179
+ };
180
+
181
+ const collectSignalScopeFrames = (
182
+ ast: AstNode,
183
+ context: FrameworkNamespaceContext
184
+ ): readonly SignalScopeFrame[] => {
185
+ const frames: SignalScopeFrame[] = [];
186
+
187
+ walk(ast, (node) => {
188
+ if (!isScopeFrameNode(node)) {
189
+ return;
190
+ }
191
+ const values = collectSignalFrameValues(node, context);
192
+ frames.push({
193
+ bindings: collectScopeFrameBindings(node),
194
+ end: node.end,
195
+ signals: values.signals,
196
+ start: node.start,
197
+ strings: values.strings,
198
+ });
199
+ });
200
+
201
+ return frames;
202
+ };
203
+
204
+ const isInsideFrame = (reference: AstNode, frame: SignalScopeFrame): boolean =>
205
+ frame.start <= reference.start && reference.end <= frame.end;
206
+
207
+ const compareInnermostFrame = (
208
+ a: SignalScopeFrame,
209
+ b: SignalScopeFrame
210
+ ): number => {
211
+ const aSize = a.end - a.start;
212
+ const bSize = b.end - b.start;
213
+ return aSize - bSize || b.start - a.start;
214
+ };
215
+
216
+ export const buildSignalIdentifierResolver = (
217
+ ast: AstNode
218
+ ): SignalIdentifierResolver => {
219
+ const context = buildFrameworkNamespaceContext(ast);
220
+ const frames = collectSignalScopeFrames(ast, context);
221
+
222
+ return {
223
+ resolve(reference: AstNode): SignalIdentifierResolution {
224
+ const name = identifierName(reference);
225
+ if (!name) {
226
+ return { kind: 'unbound' };
227
+ }
228
+
229
+ const containingFrames = frames
230
+ .filter((frame) => isInsideFrame(reference, frame))
231
+ .toSorted(compareInnermostFrame);
232
+
233
+ for (const frame of containingFrames) {
234
+ if (!frame.bindings.has(name)) {
235
+ continue;
236
+ }
237
+ const signalId = frame.signals.get(name);
238
+ if (signalId) {
239
+ return { id: signalId, kind: 'signal' };
240
+ }
241
+ const stringId = frame.strings.get(name);
242
+ if (stringId) {
243
+ return { id: stringId, kind: 'string' };
244
+ }
245
+ return { kind: 'shadowed' };
246
+ }
247
+
248
+ return { kind: 'unbound' };
249
+ },
250
+ };
251
+ };
252
+
253
+ const STORE_SIGNAL_OPERATIONS = new Set(['created', 'removed', 'updated']);
254
+
255
+ const extractStoreSignalIdFromMember = (
256
+ node: AstNode | undefined,
257
+ namedStoreTableIds: ReadonlyMap<string, string>
258
+ ): string | null => {
259
+ const member = getMemberExpression(node);
260
+ const operation = member ? getPropertyName(member.property) : null;
261
+ if (!operation || !STORE_SIGNAL_OPERATIONS.has(operation)) {
262
+ return null;
263
+ }
264
+
265
+ const signalsMember = member ? getMemberExpression(member.object) : null;
266
+ if (!signalsMember || getPropertyName(signalsMember.property) !== 'signals') {
267
+ return null;
268
+ }
269
+
270
+ const tableId = deriveStoreTableId(signalsMember.object, namedStoreTableIds);
271
+ return tableId ? `${tableId}.${operation}` : null;
272
+ };
273
+
274
+ const collectNamedStoreSignalIds = (
275
+ ast: AstNode,
276
+ namedStoreTableIds: ReadonlyMap<string, string>
277
+ ): ReadonlyMap<string, string> => {
278
+ const ids = new Map<string, string>();
279
+
280
+ walk(ast, (node) => {
281
+ if (node.type !== 'VariableDeclarator') {
282
+ return;
283
+ }
284
+
285
+ const { id, init } = node as unknown as {
286
+ readonly id?: AstNode;
287
+ readonly init?: AstNode;
288
+ };
289
+ const name = extractBindingName(id);
290
+ const signalId = extractStoreSignalIdFromMember(init, namedStoreTableIds);
291
+ if (name && signalId) {
292
+ ids.set(name, signalId);
293
+ }
294
+ });
295
+
296
+ return ids;
297
+ };
298
+
299
+ const getOnElements = (config: AstNode): readonly AstNode[] => {
300
+ const onProp = findConfigProperty(config, 'on');
301
+ if (!onProp) {
302
+ return [];
303
+ }
304
+
305
+ const arrayNode = onProp.value;
306
+ if (!arrayNode || (arrayNode as AstNode).type !== 'ArrayExpression') {
307
+ return [];
308
+ }
309
+
310
+ const elements = (arrayNode as AstNode)['elements'] as
311
+ | readonly AstNode[]
312
+ | undefined;
313
+ return elements ?? [];
314
+ };
315
+
316
+ const resolveNamedOnSignalId = (
317
+ element: AstNode,
318
+ sourceCode: string,
319
+ namedStoreSignalIds: ReadonlyMap<string, string>
320
+ ): string | null => {
321
+ if (element.type !== 'Identifier') {
322
+ return null;
323
+ }
324
+
325
+ const name = identifierName(element);
326
+ return name
327
+ ? (namedStoreSignalIds.get(name) ?? deriveConstString(name, sourceCode))
328
+ : null;
329
+ };
330
+
331
+ const resolveInlineOnSignalId = (element: AstNode): string | null => {
332
+ const definition = extractTrailDefinition(element);
333
+ return definition?.kind === 'signal' ? definition.id : null;
334
+ };
335
+
336
+ const resolveOnElementSignalId = (
337
+ element: AstNode,
338
+ sourceCode: string,
339
+ namedStoreSignalIds: ReadonlyMap<string, string>,
340
+ namedStoreTableIds: ReadonlyMap<string, string>
341
+ ): string | null => {
342
+ if (isStringLiteral(element)) {
343
+ return getStringValue(element);
344
+ }
345
+
346
+ return (
347
+ extractStoreSignalIdFromMember(element, namedStoreTableIds) ??
348
+ resolveNamedOnSignalId(element, sourceCode, namedStoreSignalIds) ??
349
+ resolveInlineOnSignalId(element)
350
+ );
351
+ };
352
+
353
+ const addOnTargetSignalIds = (
354
+ config: AstNode,
355
+ ids: Set<string>,
356
+ sourceCode: string,
357
+ namedStoreSignalIds: ReadonlyMap<string, string>,
358
+ namedStoreTableIds: ReadonlyMap<string, string>
359
+ ): void => {
360
+ for (const element of getOnElements(config)) {
361
+ const signalId = resolveOnElementSignalId(
362
+ element,
363
+ sourceCode,
364
+ namedStoreSignalIds,
365
+ namedStoreTableIds
366
+ );
367
+ if (signalId) {
368
+ ids.add(signalId);
369
+ }
370
+ }
371
+ };
372
+
373
+ export const collectOnTargetSignalIds = (
374
+ ast: AstNode,
375
+ sourceCode: string
376
+ ): ReadonlySet<string> => {
377
+ const ids = new Set<string>();
378
+ const namedStoreTableIds = collectNamedStoreTableIds(ast);
379
+ const namedStoreSignalIds = collectNamedStoreSignalIds(
380
+ ast,
381
+ namedStoreTableIds
382
+ );
383
+
384
+ for (const definition of findTrailDefinitions(ast)) {
385
+ if (definition.kind === 'trail') {
386
+ addOnTargetSignalIds(
387
+ definition.config,
388
+ ids,
389
+ sourceCode,
390
+ namedStoreSignalIds,
391
+ namedStoreTableIds
392
+ );
393
+ }
394
+ }
395
+
396
+ return ids;
397
+ };