@git-stunts/git-warp 10.8.0 → 11.3.3

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 (136) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +80 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. package/src/hooks/post-merge.sh +0 -60
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Advanced materialization methods for WarpGraph — ceiling-aware replay,
3
+ * checkpoint-based materializeAt, adjacency building, and state caching.
4
+ *
5
+ * Every function uses `this` bound to a WarpGraph instance at runtime
6
+ * via wireWarpMethods().
7
+ *
8
+ * @module domain/warp/materializeAdvanced.methods
9
+ */
10
+
11
+ import { reduceV5, createEmptyStateV5 } from '../services/JoinReducer.js';
12
+ import { orsetContains, orsetElements } from '../crdt/ORSet.js';
13
+ import { decodeEdgeKey } from '../services/KeyCodec.js';
14
+ import { vvClone } from '../crdt/VersionVector.js';
15
+ import { computeStateHashV5 } from '../services/StateSerializerV5.js';
16
+ import { ProvenanceIndex } from '../services/ProvenanceIndex.js';
17
+ import { serializeFullStateV5, deserializeFullStateV5 } from '../services/CheckpointSerializerV5.js';
18
+ import { buildSeekCacheKey } from '../utils/seekCacheKey.js';
19
+ import { materializeIncremental } from '../services/CheckpointService.js';
20
+ import { createFrontier, updateFrontier } from '../services/Frontier.js';
21
+
22
+ /** @typedef {import('../types/WarpPersistence.js').CorePersistence} CorePersistence */
23
+ /** @typedef {import('../services/JoinReducer.js').WarpStateV5} WarpStateV5 */
24
+
25
+ /**
26
+ * @typedef {{ outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>> }} AdjacencyMap
27
+ * @typedef {{ state: WarpStateV5, stateHash: string, adjacency: AdjacencyMap }} MaterializedResult
28
+ */
29
+
30
+ import { buildWriterRef } from '../utils/RefLayout.js';
31
+ import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
32
+
33
+ /**
34
+ * Resolves the effective ceiling from options and instance state.
35
+ *
36
+ * Precedence: explicit `ceiling` in options overrides the instance-level
37
+ * `_seekCeiling`. Uses the `'ceiling' in options` check, so passing
38
+ * `{ ceiling: null }` explicitly clears the seek ceiling for that call
39
+ * (returns `null`), while omitting the key falls through to `_seekCeiling`.
40
+ *
41
+ * @this {import('../WarpGraph.js').default}
42
+ * @param {{ceiling?: number|null}} [options] - Options object; when the
43
+ * `ceiling` key is present (even if `null`), its value takes precedence
44
+ * @returns {number|null} Lamport ceiling to apply, or `null` for latest
45
+ * @private
46
+ */
47
+ export function _resolveCeiling(options) {
48
+ if (options && 'ceiling' in options) {
49
+ return options.ceiling ?? null;
50
+ }
51
+ return this._seekCeiling;
52
+ }
53
+
54
+ /**
55
+ * Builds a deterministic adjacency map for the logical graph.
56
+ *
57
+ * @this {import('../WarpGraph.js').default}
58
+ * @param {import('../services/JoinReducer.js').WarpStateV5} state
59
+ * @returns {{outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}
60
+ * @private
61
+ */
62
+ export function _buildAdjacency(state) {
63
+ const outgoing = new Map();
64
+ const incoming = new Map();
65
+
66
+ for (const edgeKey of orsetElements(state.edgeAlive)) {
67
+ const { from, to, label } = decodeEdgeKey(edgeKey);
68
+
69
+ if (!orsetContains(state.nodeAlive, from) || !orsetContains(state.nodeAlive, to)) {
70
+ continue;
71
+ }
72
+
73
+ if (!outgoing.has(from)) {
74
+ outgoing.set(from, []);
75
+ }
76
+ if (!incoming.has(to)) {
77
+ incoming.set(to, []);
78
+ }
79
+
80
+ outgoing.get(from).push({ neighborId: to, label });
81
+ incoming.get(to).push({ neighborId: from, label });
82
+ }
83
+
84
+ const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
85
+ list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
86
+ if (a.neighborId !== b.neighborId) {
87
+ return a.neighborId < b.neighborId ? -1 : 1;
88
+ }
89
+ return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
90
+ });
91
+ };
92
+
93
+ for (const list of outgoing.values()) {
94
+ sortNeighbors(list);
95
+ }
96
+
97
+ for (const list of incoming.values()) {
98
+ sortNeighbors(list);
99
+ }
100
+
101
+ return { outgoing, incoming };
102
+ }
103
+
104
+ /**
105
+ * Sets the cached state and materialized graph details.
106
+ *
107
+ * @this {import('../WarpGraph.js').default}
108
+ * @param {import('../services/JoinReducer.js').WarpStateV5} state
109
+ * @returns {Promise<MaterializedResult>}
110
+ * @private
111
+ */
112
+ export async function _setMaterializedState(state) {
113
+ this._cachedState = state;
114
+ this._stateDirty = false;
115
+ this._versionVector = vvClone(state.observedFrontier);
116
+
117
+ const stateHash = await computeStateHashV5(state, { crypto: this._crypto, codec: this._codec });
118
+ let adjacency;
119
+
120
+ if (this._adjacencyCache) {
121
+ adjacency = this._adjacencyCache.get(stateHash);
122
+ if (!adjacency) {
123
+ adjacency = this._buildAdjacency(state);
124
+ this._adjacencyCache.set(stateHash, adjacency);
125
+ }
126
+ } else {
127
+ adjacency = this._buildAdjacency(state);
128
+ }
129
+
130
+ this._materializedGraph = { state, stateHash, adjacency };
131
+ return this._materializedGraph;
132
+ }
133
+
134
+ /**
135
+ * Materializes the graph with a Lamport ceiling (time-travel).
136
+ *
137
+ * Bypasses checkpoints entirely — replays all patches from all writers,
138
+ * filtering to only those with `lamport <= ceiling`. Skips auto-checkpoint
139
+ * and GC since this is an exploratory read.
140
+ *
141
+ * Uses a dedicated cache keyed on `ceiling` + frontier snapshot. Cache
142
+ * is bypassed when the writer frontier has advanced (new writers or
143
+ * updated tips) or when `collectReceipts` is `true` because the cached
144
+ * path does not retain receipt data.
145
+ *
146
+ * @this {import('../WarpGraph.js').default}
147
+ * @param {number} ceiling - Maximum Lamport tick to include (patches with
148
+ * `lamport <= ceiling` are replayed; `ceiling <= 0` yields empty state)
149
+ * @param {boolean} collectReceipts - When `true`, return receipts alongside
150
+ * state and skip the ceiling cache
151
+ * @param {number} t0 - Start timestamp for performance logging
152
+ * @returns {Promise<import('../services/JoinReducer.js').WarpStateV5 |
153
+ * {state: import('../services/JoinReducer.js').WarpStateV5,
154
+ * receipts: import('../types/TickReceipt.js').TickReceipt[]}>}
155
+ * Plain state when `collectReceipts` is falsy; `{ state, receipts }`
156
+ * when truthy
157
+ * @private
158
+ */
159
+ export async function _materializeWithCeiling(ceiling, collectReceipts, t0) {
160
+ const frontier = await this.getFrontier();
161
+
162
+ // Cache hit: same ceiling, clean state, AND frontier unchanged.
163
+ // Bypass cache when collectReceipts is true — cached path has no receipts.
164
+ const cf = this._cachedFrontier;
165
+ if (
166
+ this._cachedState && !this._stateDirty &&
167
+ ceiling === this._cachedCeiling && !collectReceipts &&
168
+ cf !== null &&
169
+ cf.size === frontier.size &&
170
+ [...frontier].every(([w, sha]) => cf.get(w) === sha)
171
+ ) {
172
+ return this._cachedState;
173
+ }
174
+
175
+ const writerIds = [...frontier.keys()];
176
+
177
+ if (writerIds.length === 0 || ceiling <= 0) {
178
+ const state = createEmptyStateV5();
179
+ this._provenanceIndex = new ProvenanceIndex();
180
+ this._provenanceDegraded = false;
181
+ await this._setMaterializedState(state);
182
+ this._cachedCeiling = ceiling;
183
+ this._cachedFrontier = frontier;
184
+ this._logTiming('materialize', t0, { metrics: '0 patches (ceiling)' });
185
+ if (collectReceipts) {
186
+ return { state, receipts: [] };
187
+ }
188
+ return state;
189
+ }
190
+
191
+ // Persistent cache check — skip when collectReceipts is requested
192
+ let cacheKey;
193
+ if (this._seekCache && !collectReceipts) {
194
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
195
+ try {
196
+ const cached = await this._seekCache.get(cacheKey);
197
+ if (cached) {
198
+ try {
199
+ const state = deserializeFullStateV5(cached, { codec: this._codec });
200
+ this._provenanceIndex = new ProvenanceIndex();
201
+ this._provenanceDegraded = true;
202
+ await this._setMaterializedState(state);
203
+ this._cachedCeiling = ceiling;
204
+ this._cachedFrontier = frontier;
205
+ this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
206
+ return state;
207
+ } catch {
208
+ // Corrupted payload — self-heal by removing the bad entry
209
+ try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
210
+ }
211
+ }
212
+ } catch {
213
+ // Cache read failed — fall through to full materialization
214
+ }
215
+ }
216
+
217
+ const allPatches = [];
218
+ for (const writerId of writerIds) {
219
+ const writerPatches = await this._loadWriterPatches(writerId);
220
+ for (const entry of writerPatches) {
221
+ if (entry.patch.lamport <= ceiling) {
222
+ allPatches.push(entry);
223
+ }
224
+ }
225
+ }
226
+
227
+ /** @type {import('../services/JoinReducer.js').WarpStateV5|undefined} */
228
+ let state;
229
+ /** @type {import('../types/TickReceipt.js').TickReceipt[]|undefined} */
230
+ let receipts;
231
+
232
+ if (allPatches.length === 0) {
233
+ state = createEmptyStateV5();
234
+ if (collectReceipts) {
235
+ receipts = [];
236
+ }
237
+ } else if (collectReceipts) {
238
+ const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches), undefined, { receipts: true }));
239
+ state = result.state;
240
+ receipts = result.receipts;
241
+ } else {
242
+ state = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches)));
243
+ }
244
+
245
+ this._provenanceIndex = new ProvenanceIndex();
246
+ for (const { patch, sha } of allPatches) {
247
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
248
+ }
249
+ this._provenanceDegraded = false;
250
+
251
+ await this._setMaterializedState(state);
252
+ this._cachedCeiling = ceiling;
253
+ this._cachedFrontier = frontier;
254
+
255
+ // Store to persistent cache (fire-and-forget — failure is non-fatal)
256
+ if (this._seekCache && !collectReceipts && allPatches.length > 0) {
257
+ if (!cacheKey) {
258
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
259
+ }
260
+ const buf = serializeFullStateV5(state, { codec: this._codec });
261
+ this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
262
+ }
263
+
264
+ // Skip auto-checkpoint and GC — this is an exploratory read
265
+ this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
266
+
267
+ if (collectReceipts) {
268
+ return { state, receipts: /** @type {import('../types/TickReceipt.js').TickReceipt[]} */ (receipts) };
269
+ }
270
+ return state;
271
+ }
272
+
273
+ /**
274
+ * Materializes the graph state at a specific checkpoint.
275
+ *
276
+ * Loads the checkpoint state and frontier, discovers current writers,
277
+ * builds the target frontier from current writer tips, and applies
278
+ * incremental patches since the checkpoint.
279
+ *
280
+ * @this {import('../WarpGraph.js').default}
281
+ * @param {string} checkpointSha - The checkpoint commit SHA
282
+ * @returns {Promise<import('../services/JoinReducer.js').WarpStateV5>} The materialized graph state at the checkpoint
283
+ * @throws {Error} If checkpoint SHA is invalid or not found
284
+ * @throws {Error} If checkpoint loading or patch decoding fails
285
+ *
286
+ * @example
287
+ * // Time-travel to a previous checkpoint
288
+ * const oldState = await graph.materializeAt('abc123');
289
+ * console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
290
+ */
291
+ export async function materializeAt(checkpointSha) {
292
+ // 1. Discover current writers to build target frontier
293
+ const writerIds = await this.discoverWriters();
294
+
295
+ // 2. Build target frontier (current tips for all writers)
296
+ const targetFrontier = createFrontier();
297
+ for (const writerId of writerIds) {
298
+ const writerRef = buildWriterRef(this._graphName, writerId);
299
+ const tipSha = await this._persistence.readRef(writerRef);
300
+ if (tipSha) {
301
+ updateFrontier(targetFrontier, writerId, tipSha);
302
+ }
303
+ }
304
+
305
+ // 3. Create a patch loader function for incremental materialization
306
+ const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
307
+ // Load patches from fromSha (exclusive) to toSha (inclusive)
308
+ // Walk from toSha back to fromSha
309
+ const patches = [];
310
+ let currentSha = toSha;
311
+
312
+ while (currentSha && currentSha !== fromSha) {
313
+ const nodeInfo = await this._persistence.getNodeInfo(currentSha);
314
+ const {message} = nodeInfo;
315
+
316
+ const kind = detectMessageKind(message);
317
+ if (kind !== 'patch') {
318
+ break;
319
+ }
320
+
321
+ const patchMeta = decodePatchMessage(message);
322
+ const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
323
+ const patch = this._codec.decode(patchBuffer);
324
+
325
+ patches.push({ patch, sha: currentSha });
326
+
327
+ if (nodeInfo.parents && nodeInfo.parents.length > 0) {
328
+ currentSha = nodeInfo.parents[0];
329
+ } else {
330
+ break;
331
+ }
332
+ }
333
+
334
+ return patches.reverse();
335
+ };
336
+
337
+ // 4. Call materializeIncremental with the checkpoint and target frontier
338
+ /** @type {CorePersistence} */
339
+ const persistence = this._persistence;
340
+ const state = await materializeIncremental({
341
+ persistence,
342
+ graphName: this._graphName,
343
+ checkpointSha,
344
+ targetFrontier,
345
+ patchLoader,
346
+ codec: this._codec,
347
+ });
348
+ await this._setMaterializedState(state);
349
+ return state;
350
+ }