@inflexa-ai/harness 0.18.0 → 0.19.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 (41) hide show
  1. package/dist/agents/report-session-agent.d.ts +56 -0
  2. package/dist/agents/report-session-agent.js +72 -0
  3. package/dist/app/report-session-runtime.d.ts +72 -0
  4. package/dist/app/report-session-runtime.js +157 -0
  5. package/dist/memory/thread-store.d.ts +4 -4
  6. package/dist/memory/thread-store.js +18 -9
  7. package/dist/prompts/report-session.d.ts +1 -0
  8. package/dist/prompts/report-session.js +89 -0
  9. package/dist/providers/ai-sdk.d.ts +77 -0
  10. package/dist/providers/ai-sdk.js +149 -19
  11. package/dist/providers/errors.d.ts +14 -0
  12. package/dist/providers/errors.js +103 -18
  13. package/dist/providers/types.d.ts +16 -0
  14. package/dist/providers/types.js +10 -1
  15. package/dist/report-model/{mint-snapshot.d.ts → pin-snapshot.d.ts} +7 -7
  16. package/dist/report-model/{mint-snapshot.js → pin-snapshot.js} +7 -7
  17. package/dist/report-model/reference-resolver.d.ts +1 -1
  18. package/dist/report-model/validate.d.ts +34 -2
  19. package/dist/report-model/validate.js +32 -17
  20. package/dist/report-render/value-bridge.d.ts +62 -0
  21. package/dist/report-render/value-bridge.js +82 -0
  22. package/dist/runtime/assemble.d.ts +15 -0
  23. package/dist/runtime/assemble.js +28 -6
  24. package/dist/state/init.js +23 -0
  25. package/dist/state/purge-analysis.js +1 -0
  26. package/dist/state/report-session-state.d.ts +112 -0
  27. package/dist/state/report-session-state.js +150 -0
  28. package/dist/state/report-versions.d.ts +3 -6
  29. package/dist/state/report-versions.js +11 -38
  30. package/dist/state/snapshot-parse.d.ts +34 -0
  31. package/dist/state/snapshot-parse.js +82 -0
  32. package/dist/tools/ad-hoc-router.d.ts +7 -2
  33. package/dist/tools/ad-hoc-router.js +2 -1
  34. package/dist/tools/report-authoring/authoring-tools.d.ts +131 -24
  35. package/dist/tools/report-authoring/authoring-tools.js +152 -44
  36. package/dist/tools/report-session/index.d.ts +8 -0
  37. package/dist/tools/report-session/index.js +8 -0
  38. package/dist/tools/report-session/preview-report.d.ts +90 -0
  39. package/dist/tools/report-session/preview-report.js +245 -0
  40. package/dist/tools/research/generate-plan.js +4 -2
  41. package/package.json +1 -1
@@ -1,19 +1,24 @@
1
1
  /**
2
2
  * The authoring tool surface over the draft operations.
3
3
  *
4
- * The factory closes over a private draft holder and the pinned snapshot. It gives eight tools and a
5
- * read of the current draft. The tools read no ambient state, thus two factories hold two independent
6
- * drafts.
4
+ * The factory takes a session-state gateway, and it holds no draft. Each tool reads the thread id from the
5
+ * scope of the call, loads the state of that thread, applies the pure operation, and persists the new
6
+ * document. Thus two threads never share one draft, and a landed document outlives a host restart.
7
7
  *
8
- * Each tool maps its flat input onto a core operation, and it applies the pure operation. The holder
9
- * swaps only on a landed operation. A refused operation returns typed data in the ok channel, and the
10
- * error channel stays for an unexpected failure.
8
+ * Each tool maps its flat input onto a core operation, and it applies the pure operation. A landed
9
+ * document persists before the tool reports `applied: true`. A refused operation returns typed data in the
10
+ * ok channel, and the error channel stays for an unexpected failure.
11
+ *
12
+ * A call with no report thread in its scope, a thread with no stored state, and a gateway fault each
13
+ * refuse as typed data too. That refusal sits beside the core refusal, and it names the absent thread
14
+ * scope or the absent state.
11
15
  *
12
16
  * The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
13
17
  * nested union. A conflict between the flat fields refuses before the core runs.
14
18
  */
15
19
  import { err, ok } from "neverthrow";
16
20
  import { z } from "zod";
21
+ import { isSafeId } from "../../workspace/paths.js";
17
22
  import { addBlock, changeBlock, locate, moveBlock, removeBlock, setTitle, } from "../../report-model/draft-operations.js";
18
23
  import { buildOutline, childOutline, readBlock } from "../../report-model/draft-read.js";
19
24
  import { finishDraft } from "../../report-model/draft-finish.js";
@@ -78,6 +83,44 @@ const setTitleInput = z.object({
78
83
  title: z.string().min(1),
79
84
  });
80
85
  const finishDraftInput = z.object({});
86
+ /**
87
+ * Resolve the report thread of a call, and load its state through the gateway.
88
+ *
89
+ * The report thread and its analysis ride on the analysis scope. The thread id becomes one segment of a
90
+ * session directory, thus the safe-id check sits here beside the shape check: a scope whose id carries a
91
+ * separator or a traversal segment names no thread that a tool can write under.
92
+ *
93
+ * The stored analysis of the thread must match the scope. A mismatch reads one analysis's draft and writes
94
+ * into another analysis's workspace, thus the resolution refuses it with a permanent reason that names the
95
+ * two ids. A scope of a different kind, an unsafe id, an absent thread, a wrong thread type, and a gateway
96
+ * fault each refuse as a typed `SessionRefusal` too.
97
+ *
98
+ * The authoring tools and the preview tool share this one resolution, thus the two surfaces accept the
99
+ * same thread id and map a load outcome onto a refusal the same way.
100
+ */
101
+ export async function openReportThread(gateway, scope) {
102
+ if (scope.kind !== "analysis" || scope.threadId === undefined || !isSafeId(scope.threadId)) {
103
+ return err({ reason: "no-thread-scope", detail: "the scope of the call names no usable report thread id" });
104
+ }
105
+ const { threadId, analysisId } = scope;
106
+ const loaded = await gateway.load(threadId);
107
+ if (loaded.outcome === "absent") {
108
+ return err({ reason: "absent-state", detail: `no report session exists for the thread ${threadId}, and this condition is permanent` });
109
+ }
110
+ if (loaded.outcome === "wrong-type") {
111
+ return err({ reason: "wrong-thread-type", detail: `${loaded.detail}, and this condition is permanent` });
112
+ }
113
+ if (loaded.outcome === "failed") {
114
+ return err({ reason: "state-unavailable", detail: loaded.detail });
115
+ }
116
+ if (loaded.analysisId !== analysisId) {
117
+ return err({
118
+ reason: "scope-analysis-mismatch",
119
+ detail: `the scope names the analysis ${analysisId}, but the thread ${threadId} belongs to the analysis ${loaded.analysisId}`,
120
+ });
121
+ }
122
+ return ok({ threadId, analysisId, state: loaded.state, token: loaded.token });
123
+ }
81
124
  /** Read an absentable field. An explicit `null` and an omitted key both mean that the model gave no value. */
82
125
  function given(value) {
83
126
  return value ?? undefined;
@@ -197,37 +240,59 @@ function containerIn(document, parentId) {
197
240
  /**
198
241
  * Every tool of this surface runs inline.
199
242
  *
200
- * The draft is closure memory with no durable backing. A step-mode tool goes through `DBOS.runStep`, which
201
- * caches the result of a step and replays it without running the body. A replay after a host restart would
202
- * hand the agent the cached `applied: true` outline of each landed operation while the rebuilt closure
203
- * holds an empty draft, and the finish would then report an empty document for a report that the
204
- * transcript shows as complete. Inline mode keeps the tool result and the draft in one lifetime.
243
+ * A tool loads the state of its thread, applies the pure operation, and persists the result in one body.
244
+ * The reported outline must agree with the row that the body just wrote. A step-mode tool goes through
245
+ * `DBOS.runStep`, which caches the result of a step and replays it without a fresh load. A replay could
246
+ * then report an outline that the current row no longer holds. Inline mode reads and writes the row in one
247
+ * lifetime, thus the outline never drifts from the state.
205
248
  */
206
249
  const AUTHORING_EXECUTION_MODE = "inline";
207
250
  /**
208
- * Make the eight authoring tools over one draft.
251
+ * Make the eight authoring tools over a session-state gateway.
209
252
  *
210
- * The holder is a private `let`. Each tool reads the holder at call time, thus a landing through one tool
211
- * shows in the next call of every tool. The holder swaps only inside `land`, thus a refused operation
212
- * leaves the draft as it was.
253
+ * Each tool reads the thread id from the scope of the call, and it loads the state of that thread through
254
+ * the gateway. Thus one factory serves every thread, and two threads never share one draft. A mutation
255
+ * persists the new document before it reports `applied: true`, thus a reported landing is never lost.
213
256
  */
214
- export function createReportAuthoringTools(state) {
215
- const snapshot = state.snapshot;
216
- let draft = state.initialDraft ?? { title: "", sections: [] };
257
+ export function createReportAuthoringTools(gateway) {
258
+ /**
259
+ * Resolve the thread of the call, and load its state.
260
+ *
261
+ * The shared `openReportThread` holds the one resolution. A scope of a different kind, an unsafe or
262
+ * absent thread id, an absent row, and a gateway fault each refuse before the call reaches a core
263
+ * operation.
264
+ */
265
+ const openThread = (ctx) => openReportThread(gateway, ctx.session.scope);
217
266
  /**
218
- * Swap the holder on a landing, and report the containers that the operation changed.
267
+ * Persist a landing, and report the containers that the operation changed.
219
268
  *
220
- * `holders` names them by id, and it reads the draft as it was and the draft as it becomes, because a
221
- * removed block has a holder only in the first and an added block has one only in the second. The
222
- * child order comes out of `next` in either case.
269
+ * `holders` names them by id, and it reads the previous document and the next document, because a
270
+ * removed block has a holder only in the first and an added block has one only in the second. The child
271
+ * order comes out of `next` in either case. A persist fault refuses with `state-unavailable`, thus the
272
+ * tool reports `applied: true` only after the row holds the new document.
273
+ *
274
+ * The persist is a compare-and-swap against the token that the load read. A concurrent turn that landed
275
+ * first turns the persist into a conflict, and the tool refuses with `stale-state`.
223
276
  */
224
- const land = (result, holders) => result.match((next) => {
225
- const previous = draft;
226
- draft = next;
277
+ const land = async (threadId, token, previous, result, holders) => {
278
+ if (result.isErr()) {
279
+ return { applied: false, refusal: result.error };
280
+ }
281
+ const next = result.value;
282
+ const persisted = await gateway.persist(threadId, next, token);
283
+ if (persisted.outcome === "conflict") {
284
+ return {
285
+ applied: false,
286
+ refusal: { reason: "stale-state", detail: "another turn changed the report since this turn read it, thus read the state again" },
287
+ };
288
+ }
289
+ if (persisted.outcome === "failed") {
290
+ return { applied: false, refusal: { reason: "state-unavailable", detail: persisted.detail } };
291
+ }
227
292
  // A move inside one container names the same holder two times.
228
293
  const unique = [...new Set(holders(previous, next))];
229
294
  return { applied: true, changed: unique.map((parentId) => containerIn(next, parentId)) };
230
- }, (refusal) => ({ applied: false, refusal }));
295
+ };
231
296
  const refuse = (refusal) => ({ applied: false, refusal });
232
297
  const add_block = defineTool({
233
298
  id: "add_block",
@@ -241,16 +306,19 @@ export function createReportAuthoringTools(state) {
241
306
  const kind = readKind(decodeBlockPayload(input.block));
242
307
  return kind !== undefined ? `add ${kind}` : "add a block";
243
308
  },
244
- execute: async (input) => {
309
+ execute: async (input, ctx) => {
310
+ const opened = await openThread(ctx);
311
+ if (opened.isErr()) {
312
+ return ok(refuse(opened.error));
313
+ }
314
+ const { threadId, state, token } = opened.value;
245
315
  const destination = toDestination(input);
246
316
  if (destination.isErr()) {
247
317
  return ok(refuse(destination.error));
248
318
  }
249
319
  const block = decodeBlockPayload(input.block);
250
320
  const addedId = readId(block);
251
- return ok(land(addBlock(draft, { block, destination: destination.value }, snapshot), (_previous, next) => [
252
- addedId === undefined ? undefined : holderIdOf(next, addedId),
253
- ]));
321
+ return ok(await land(threadId, token, state.document, addBlock(state.document, { block, destination: destination.value }, state.snapshot), (_previous, next) => [addedId === undefined ? undefined : holderIdOf(next, addedId)]));
254
322
  },
255
323
  });
256
324
  const change_block = defineTool({
@@ -260,12 +328,19 @@ export function createReportAuthoringTools(state) {
260
328
  inputSchema: changeBlockInput,
261
329
  executionMode: AUTHORING_EXECUTION_MODE,
262
330
  describeCall: (input) => `change ${input.targetId}`,
263
- execute: async (input) => {
331
+ execute: async (input, ctx) => {
332
+ const opened = await openThread(ctx);
333
+ if (opened.isErr()) {
334
+ return ok(refuse(opened.error));
335
+ }
336
+ const { threadId, state, token } = opened.value;
264
337
  const operation = toChangeOperation(input);
265
338
  if (operation.isErr()) {
266
339
  return ok(refuse(operation.error));
267
340
  }
268
- return ok(land(changeBlock(draft, operation.value, snapshot), (_previous, next) => [holderIdOf(next, input.targetId)]));
341
+ return ok(await land(threadId, token, state.document, changeBlock(state.document, operation.value, state.snapshot), (_previous, next) => [
342
+ holderIdOf(next, input.targetId),
343
+ ]));
269
344
  },
270
345
  });
271
346
  const remove_block = defineTool({
@@ -274,7 +349,16 @@ export function createReportAuthoringTools(state) {
274
349
  inputSchema: removeBlockInput,
275
350
  executionMode: AUTHORING_EXECUTION_MODE,
276
351
  describeCall: (input) => `remove ${input.targetId}`,
277
- execute: async (input) => ok(land(removeBlock(draft, { targetId: input.targetId }, snapshot), (previous) => [holderIdOf(previous, input.targetId)])),
352
+ execute: async (input, ctx) => {
353
+ const opened = await openThread(ctx);
354
+ if (opened.isErr()) {
355
+ return ok(refuse(opened.error));
356
+ }
357
+ const { threadId, state, token } = opened.value;
358
+ return ok(await land(threadId, token, state.document, removeBlock(state.document, { targetId: input.targetId }, state.snapshot), (previous) => [
359
+ holderIdOf(previous, input.targetId),
360
+ ]));
361
+ },
278
362
  });
279
363
  const move_block = defineTool({
280
364
  id: "move_block",
@@ -283,15 +367,17 @@ export function createReportAuthoringTools(state) {
283
367
  inputSchema: moveBlockInput,
284
368
  executionMode: AUTHORING_EXECUTION_MODE,
285
369
  describeCall: (input) => `move ${input.targetId}`,
286
- execute: async (input) => {
370
+ execute: async (input, ctx) => {
371
+ const opened = await openThread(ctx);
372
+ if (opened.isErr()) {
373
+ return ok(refuse(opened.error));
374
+ }
375
+ const { threadId, state, token } = opened.value;
287
376
  const destination = toDestination(input);
288
377
  if (destination.isErr()) {
289
378
  return ok(refuse(destination.error));
290
379
  }
291
- return ok(land(moveBlock(draft, { targetId: input.targetId, destination: destination.value }, snapshot), (previous, next) => [
292
- holderIdOf(previous, input.targetId),
293
- holderIdOf(next, input.targetId),
294
- ]));
380
+ return ok(await land(threadId, token, state.document, moveBlock(state.document, { targetId: input.targetId, destination: destination.value }, state.snapshot), (previous, next) => [holderIdOf(previous, input.targetId), holderIdOf(next, input.targetId)]));
295
381
  },
296
382
  });
297
383
  const set_title = defineTool({
@@ -302,7 +388,14 @@ export function createReportAuthoringTools(state) {
302
388
  executionMode: AUTHORING_EXECUTION_MODE,
303
389
  describeCall: (input) => `title the report ${input.title}`,
304
390
  // The title sits on the document, thus no container changes its child order.
305
- execute: async (input) => ok(land(ok(setTitle(draft, input.title)), () => [])),
391
+ execute: async (input, ctx) => {
392
+ const opened = await openThread(ctx);
393
+ if (opened.isErr()) {
394
+ return ok(refuse(opened.error));
395
+ }
396
+ const { threadId, state, token } = opened.value;
397
+ return ok(await land(threadId, token, state.document, ok(setTitle(state.document, input.title)), () => []));
398
+ },
306
399
  });
307
400
  const read_outline = defineTool({
308
401
  id: "read_outline",
@@ -313,7 +406,13 @@ export function createReportAuthoringTools(state) {
313
406
  executionMode: AUTHORING_EXECUTION_MODE,
314
407
  // The input carries no field, thus a hook can only restate the name of the tool.
315
408
  describeCall: "none",
316
- execute: async () => ok({ outline: buildOutline(draft) }),
409
+ execute: async (_input, ctx) => {
410
+ const opened = await openThread(ctx);
411
+ if (opened.isErr()) {
412
+ return ok({ refused: opened.error });
413
+ }
414
+ return ok({ outline: buildOutline(opened.value.state.document) });
415
+ },
317
416
  });
318
417
  const read_block = defineTool({
319
418
  id: "read_block",
@@ -322,8 +421,12 @@ export function createReportAuthoringTools(state) {
322
421
  inputSchema: readBlockInput,
323
422
  executionMode: AUTHORING_EXECUTION_MODE,
324
423
  describeCall: (input) => `read ${input.targetId}`,
325
- execute: async (input) => {
326
- const block = readBlock(draft, input.targetId);
424
+ execute: async (input, ctx) => {
425
+ const opened = await openThread(ctx);
426
+ if (opened.isErr()) {
427
+ return ok({ refused: opened.error });
428
+ }
429
+ const block = readBlock(opened.value.state.document, input.targetId);
327
430
  const result = block !== undefined ? { found: true, block } : { found: false };
328
431
  return ok(result);
329
432
  },
@@ -336,7 +439,13 @@ export function createReportAuthoringTools(state) {
336
439
  executionMode: AUTHORING_EXECUTION_MODE,
337
440
  // The input carries no field, thus a hook can only restate the name of the tool.
338
441
  describeCall: "none",
339
- execute: async () => ok(finishDraft(draft, snapshot)),
442
+ execute: async (_input, ctx) => {
443
+ const opened = await openThread(ctx);
444
+ if (opened.isErr()) {
445
+ return ok({ refused: opened.error });
446
+ }
447
+ return ok(finishDraft(opened.value.state.document, opened.value.state.snapshot));
448
+ },
340
449
  });
341
450
  return {
342
451
  add_block,
@@ -347,6 +456,5 @@ export function createReportAuthoringTools(state) {
347
456
  read_outline,
348
457
  read_block,
349
458
  finish_draft,
350
- currentDraft: () => draft,
351
459
  };
352
460
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The report-session tool barrel.
3
+ *
4
+ * These tools serve a `report` thread. The runtime constructs them over the session-state gateway and the
5
+ * render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
6
+ * the factories and their public types for the composition root.
7
+ */
8
+ export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps } from "./preview-report.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The report-session tool barrel.
3
+ *
4
+ * These tools serve a `report` thread. The runtime constructs them over the session-state gateway and the
5
+ * render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
6
+ * the factories and their public types for the composition root.
7
+ */
8
+ export { createPreviewReportTool } from "./preview-report.js";
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The render-and-preview tool of a report session.
3
+ *
4
+ * The tool renders the current draft of a thread. It runs the finish first, because a mid-composition
5
+ * draft is not a `ReportDocument`. A gap list returns as data, and no render runs. On a pass the tool
6
+ * resolves each reference through the injected resolver, bridges the resolved values into the render model,
7
+ * and renders the page.
8
+ *
9
+ * The page and its staged image assets land in `report-sessions/{threadId}/` under the workspace root. That
10
+ * namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
11
+ * trees. The result carries the absolute page path, thus a local host shows the page with no seam.
12
+ *
13
+ * The result carries no access grant. `PreviewPublisher` authorizes the URL space
14
+ * `previews/{analysisId}/{previewId}` (`contracts/content-url.ts`), and that space cannot name a page of
15
+ * this tree. A hosted view of a session page is a later capability, with a URL space of its own.
16
+ *
17
+ * Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
18
+ * absence, an unresolved reference, a bridge mismatch, a render problem, a figure that escapes the
19
+ * workspace root, and a write failure. The tool never throws for one of them. The filesystem speaks the
20
+ * throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
21
+ * write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
22
+ * control-flow exception propagate.
23
+ */
24
+ import { z } from "zod";
25
+ import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
26
+ import { type Logger } from "../../lib/logger.js";
27
+ import { type FinishGap } from "../../report-model/draft-finish.js";
28
+ import type { ReferenceResolver } from "../../report-model/reference-resolver.js";
29
+ import { type ResolutionFailure } from "../../report-model/validate.js";
30
+ import { type BridgeMismatch } from "../../report-render/value-bridge.js";
31
+ import type { RenderProblem } from "../../report-render/types.js";
32
+ import { type Tool } from "../define-tool.js";
33
+ import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
34
+ /** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
35
+ declare const previewReportInput: z.ZodObject<{}, z.core.$strip>;
36
+ export type PreviewReportInput = z.infer<typeof previewReportInput>;
37
+ /**
38
+ * The typed outcome of the preview tool. Each arm is ok-channel data, thus the tool never throws for a
39
+ * degraded condition. `rendered` carries the absolute page path.
40
+ */
41
+ export type PreviewReportResult = {
42
+ outcome: "refused";
43
+ refusal: SessionRefusal;
44
+ } | {
45
+ outcome: "gaps";
46
+ gaps: FinishGap[];
47
+ } | {
48
+ outcome: "resolver-unavailable";
49
+ } | {
50
+ outcome: "unresolved-references";
51
+ unresolved: ResolutionFailure[];
52
+ } | {
53
+ outcome: "bridge-mismatch";
54
+ mismatches: BridgeMismatch[];
55
+ } | {
56
+ outcome: "render-problems";
57
+ problems: RenderProblem[];
58
+ } | {
59
+ outcome: "figure-out-of-scope";
60
+ blockId: string;
61
+ path: string;
62
+ } | {
63
+ outcome: "write-failed";
64
+ detail: string;
65
+ } | {
66
+ outcome: "rendered";
67
+ pagePath: string;
68
+ };
69
+ /**
70
+ * The construction deps of the preview tool.
71
+ *
72
+ * `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
73
+ * serves every analysis and it resolves the root per call from the scope. `resolver` is optional, because a
74
+ * resolver realization can be absent.
75
+ */
76
+ export interface PreviewReportToolDeps {
77
+ readonly gateway: ReportSessionStateGateway;
78
+ readonly resolver?: ReferenceResolver;
79
+ readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
80
+ readonly logger?: Logger;
81
+ }
82
+ /**
83
+ * Make the render-and-preview tool over the session-state gateway and the render seams.
84
+ *
85
+ * The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
86
+ * one factory serves every thread. The tool holds no per-session value, and it resolves the root of the
87
+ * analysis of the call.
88
+ */
89
+ export declare function createPreviewReportTool(deps: PreviewReportToolDeps): Tool<PreviewReportInput, PreviewReportResult>;
90
+ export {};