sh3-core 0.23.2 → 0.25.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 (40) hide show
  1. package/dist/BrandSlot.svelte +62 -3
  2. package/dist/BrandSlot.test.js +52 -0
  3. package/dist/Sh3.svelte +4 -4
  4. package/dist/actions/listActive.js +1 -0
  5. package/dist/actions/listActive.test.js +13 -0
  6. package/dist/actions/types.d.ts +12 -0
  7. package/dist/api.d.ts +2 -0
  8. package/dist/api.js +2 -0
  9. package/dist/app/store/StoreView.svelte +1 -1
  10. package/dist/apps/types.d.ts +8 -0
  11. package/dist/chrome/MenuSheet.svelte +19 -6
  12. package/dist/contributions/contextSource.d.ts +48 -0
  13. package/dist/contributions/contextSource.js +21 -0
  14. package/dist/documents/picker-primitive.d.ts +0 -9
  15. package/dist/documents/picker-primitive.js +0 -9
  16. package/dist/layout/store.svelte.js +1 -1
  17. package/dist/overlays/presets.d.ts +17 -2
  18. package/dist/overlays/presets.js +28 -2
  19. package/dist/overlays/presets.test.js +29 -0
  20. package/dist/primitives/widgets/DocumentFilePicker.svelte +9 -7
  21. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +44 -27
  22. package/dist/primitives/widgets/_DocumentBrowser.svelte +4 -4
  23. package/dist/registry/installer.js +50 -10
  24. package/dist/registry/installer.test.d.ts +1 -0
  25. package/dist/registry/installer.test.js +146 -0
  26. package/dist/registry/types.d.ts +19 -0
  27. package/dist/runtime/runVerb.test.js +87 -0
  28. package/dist/sh3core-shard/Sh3Home.svelte +0 -1
  29. package/dist/shards/lifecycle.svelte.d.ts +8 -0
  30. package/dist/shards/lifecycle.svelte.js +17 -0
  31. package/dist/shell-shard/verbs/xfer.js +66 -4
  32. package/dist/shell-shard/verbs/xfer.test.js +74 -0
  33. package/dist/transport/apiFetch.js +21 -3
  34. package/dist/transport/apiFetch.test.js +63 -0
  35. package/dist/verbs/types.d.ts +49 -12
  36. package/dist/verbs/types.test.d.ts +1 -0
  37. package/dist/verbs/types.test.js +43 -0
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +1 -1
@@ -1,4 +1,38 @@
1
1
  import { parseScopePath, resolveScope } from './scope-parse';
2
+ // ---------------------------------------------------------------------------
3
+ // Path helpers
4
+ //
5
+ // xfer follows cp-style semantics for the destination:
6
+ // - dst path ending with `/` (or empty) is a directory; the file is placed
7
+ // inside with the source's filename (or, for -R, with the source-relative
8
+ // path rebased under the dst directory).
9
+ // - dst path without trailing slash is treated literally.
10
+ // ---------------------------------------------------------------------------
11
+ function basename(p) {
12
+ const i = p.lastIndexOf('/');
13
+ return i >= 0 ? p.slice(i + 1) : p;
14
+ }
15
+ /** Compose a final dst path from a (possibly-directory) dst dir plus a relative segment. */
16
+ function joinDst(dstDir, relative) {
17
+ if (!dstDir)
18
+ return relative;
19
+ if (dstDir.endsWith('/'))
20
+ return dstDir + relative;
21
+ return `${dstDir}/${relative}`;
22
+ }
23
+ /**
24
+ * Strip a folder prefix off a doc path, returning the remainder. For the
25
+ * single-file case (doc.path equals prefix), returns the filename. The
26
+ * caller's filter guarantees doc.path is either exactly `prefix` or starts
27
+ * with `prefix + '/'`, so this never produces a stray leading slash.
28
+ */
29
+ function rebaseFromPrefix(prefix, docPath) {
30
+ if (!prefix)
31
+ return docPath;
32
+ if (docPath === prefix)
33
+ return basename(docPath);
34
+ return docPath.slice(prefix.length + 1); // skip prefix and the separating '/'
35
+ }
2
36
  export const xferVerb = {
3
37
  name: 'xfer',
4
38
  summary: [
@@ -7,6 +41,7 @@ export const xferVerb = {
7
41
  ' Either side may be @me or @project-<slug>; bare paths resolve to the active scope.',
8
42
  ' -R recursive (src is a folder prefix)',
9
43
  ' -C copy only, do not delete source',
44
+ ' Trailing `/` on dst means "into directory" (cp-style); without it dst is a literal path.',
10
45
  ].join('\n'),
11
46
  programmatic: true,
12
47
  async run(ctx, args) {
@@ -60,25 +95,52 @@ export const xferVerb = {
60
95
  ctx.scrollback.push({ kind: 'status', text: 'xfer: path required (use -R for folder recursion)', level: 'error', ts });
61
96
  return;
62
97
  }
63
- if (srcTenant === dstTenant && srcParsed.shardId === dstParsed.shardId && srcParsed.path === dstParsed.path) {
98
+ // cp-style: trailing slash (or empty path) means "into this directory"
99
+ // append the source filename so the file lands inside instead of trying
100
+ // to overwrite the directory itself.
101
+ const dstIsDir = !dstParsed.path || dstParsed.path.endsWith('/');
102
+ const dstFinalPath = dstIsDir
103
+ ? joinDst(dstParsed.path, basename(srcParsed.path))
104
+ : dstParsed.path;
105
+ if (srcTenant === dstTenant && srcParsed.shardId === dstParsed.shardId && srcParsed.path === dstFinalPath) {
64
106
  ctx.scrollback.push({ kind: 'status', text: 'xfer: source and destination are the same', level: 'error', ts });
65
107
  return;
66
108
  }
67
- await ctx.sh3.docs.transferBetweenScopes(srcTenant, srcParsed.shardId, srcParsed.path, dstTenant, dstParsed.shardId, dstParsed.path, moveOpts);
109
+ await ctx.sh3.docs.transferBetweenScopes(srcTenant, srcParsed.shardId, srcParsed.path, dstTenant, dstParsed.shardId, dstFinalPath, moveOpts);
68
110
  const verb = copy ? 'copied' : 'moved';
69
111
  ctx.scrollback.push({ kind: 'status', text: `xfer: ${verb} ${positional[0]} → ${positional[1]}`, level: 'info', ts });
70
112
  return;
71
113
  }
72
114
  const prefix = srcParsed.path;
73
115
  const allDocs = await ctx.sh3.docs.listDocumentsIn(srcTenant);
74
- const matching = allDocs.filter((d) => d.shardId === srcParsed.shardId && (!prefix || d.path.startsWith(prefix)));
116
+ // Folder-boundary filter: when `prefix` is set, a doc matches only if its
117
+ // path equals the prefix (single-file case) or sits inside the prefix
118
+ // folder (`prefix + '/'` ...). Plain `startsWith(prefix)` would also
119
+ // match sibling files whose names happen to share the prefix string
120
+ // (e.g. prefix `notes` matching `notesheet.md`).
121
+ const matching = allDocs.filter((d) => {
122
+ if (d.shardId !== srcParsed.shardId)
123
+ return false;
124
+ if (!prefix)
125
+ return true;
126
+ if (d.path === prefix)
127
+ return true;
128
+ return d.path.startsWith(`${prefix}/`);
129
+ });
75
130
  if (matching.length === 0) {
76
131
  ctx.scrollback.push({ kind: 'status', text: `xfer: no documents found under ${positional[0]}`, level: 'info', ts });
77
132
  return;
78
133
  }
79
134
  let count = 0;
80
135
  for (const doc of matching) {
81
- await ctx.sh3.docs.transferBetweenScopes(srcTenant, doc.shardId, doc.path, dstTenant, dstParsed.shardId, doc.path, moveOpts);
136
+ // Per-doc dst: rebase the doc's path relative to the src prefix, then
137
+ // place it under the dst directory. Without this the dst directory is
138
+ // ignored and the file lands at its source path in dst shard, which
139
+ // (a) silently misplaces the file and (b) blows up on mounts where
140
+ // `mounts/<unknown-segment>/...` fails to resolve to a real mount.
141
+ const relative = rebaseFromPrefix(prefix, doc.path);
142
+ const dstFinalPath = joinDst(dstParsed.path, relative);
143
+ await ctx.sh3.docs.transferBetweenScopes(srcTenant, doc.shardId, doc.path, dstTenant, dstParsed.shardId, dstFinalPath, moveOpts);
82
144
  count++;
83
145
  }
84
146
  const verb = copy ? 'copied' : 'moved';
@@ -104,4 +104,78 @@ describe('xfer verb', () => {
104
104
  expect(transferBetweenScopes).toHaveBeenCalledTimes(2);
105
105
  expect(transferBetweenScopes).toHaveBeenCalledWith('proj-abc', 'notes', 'ideas/a.md', 'user-me', 'notes', 'ideas/a.md', expect.objectContaining({ delete: true }));
106
106
  });
107
+ // ---------------------------------------------------------------------------
108
+ // Bug repro — xfer -R discards dst directory; dst with trailing slash should
109
+ // preserve it; substring-prefix filter false-matches.
110
+ // ---------------------------------------------------------------------------
111
+ it('-R preserves dst directory when transferring into a different folder', async () => {
112
+ // User's exact scenario: a single file under svg-designer goes into a
113
+ // mount subdirectory. Previously dst.path was discarded and the file
114
+ // landed at `mounts/sh3_dirt.svg`, which the mount resolver interpreted
115
+ // as `mountId=sh3_dirt.svg` → 500.
116
+ const transferBetweenScopes = vi.fn(async () => { });
117
+ const allDocs = [
118
+ { shardId: 'svg-designer', path: 'sh3_dirt.svg', size: 1, lastModified: 0 },
119
+ ];
120
+ const docs = makeDocs({
121
+ transferBetweenScopes,
122
+ listDocumentsIn: vi.fn(async () => allDocs),
123
+ });
124
+ const sh3 = makeSh3(personalScope);
125
+ const { ctx } = makeCtx(docs, sh3);
126
+ await xferVerb.run(ctx, ['-R', '-C', '@me:svg-designer/sh3_dirt.svg', 'mounts/square-survivor/']);
127
+ expect(transferBetweenScopes).toHaveBeenCalledTimes(1);
128
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'svg-designer', 'sh3_dirt.svg', 'user-me', 'mounts', 'square-survivor/sh3_dirt.svg', expect.objectContaining({ delete: false }));
129
+ });
130
+ it('-R rebases doc paths under a folder prefix into the dst directory', async () => {
131
+ const transferBetweenScopes = vi.fn(async () => { });
132
+ const allDocs = [
133
+ { shardId: 'notes', path: 'ideas/a.md', size: 1, lastModified: 0 },
134
+ { shardId: 'notes', path: 'ideas/sub/b.md', size: 1, lastModified: 0 },
135
+ ];
136
+ const docs = makeDocs({
137
+ transferBetweenScopes,
138
+ listDocumentsIn: vi.fn(async () => allDocs),
139
+ });
140
+ const sh3 = makeSh3(personalScope);
141
+ const { ctx } = makeCtx(docs, sh3);
142
+ await xferVerb.run(ctx, ['-R', '@me:notes/ideas', '@me:notes/archived/']);
143
+ expect(transferBetweenScopes).toHaveBeenCalledTimes(2);
144
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'ideas/a.md', 'user-me', 'notes', 'archived/a.md', expect.anything());
145
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'ideas/sub/b.md', 'user-me', 'notes', 'archived/sub/b.md', expect.anything());
146
+ });
147
+ it('-R filter respects folder boundary — does not match substring prefixes', async () => {
148
+ const transferBetweenScopes = vi.fn(async () => { });
149
+ const allDocs = [
150
+ { shardId: 'notes', path: 'notes/draft.md', size: 1, lastModified: 0 },
151
+ { shardId: 'notes', path: 'notesheet.md', size: 1, lastModified: 0 },
152
+ ];
153
+ const docs = makeDocs({
154
+ transferBetweenScopes,
155
+ listDocumentsIn: vi.fn(async () => allDocs),
156
+ });
157
+ const sh3 = makeSh3(personalScope);
158
+ const { ctx } = makeCtx(docs, sh3);
159
+ await xferVerb.run(ctx, ['-R', '@me:notes/notes', '@me:notes/archived/']);
160
+ // Only notes/draft.md matches the folder prefix `notes`. notesheet.md
161
+ // shares the substring but is a sibling file, not a child.
162
+ expect(transferBetweenScopes).toHaveBeenCalledTimes(1);
163
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'notes/draft.md', 'user-me', 'notes', 'archived/draft.md', expect.anything());
164
+ });
165
+ it('non-recursive: trailing-slash dst appends source filename', async () => {
166
+ const transferBetweenScopes = vi.fn(async () => { });
167
+ const docs = makeDocs({ transferBetweenScopes });
168
+ const sh3 = makeSh3(personalScope);
169
+ const { ctx } = makeCtx(docs, sh3);
170
+ await xferVerb.run(ctx, ['@me:notes/foo.md', '@me:notes/archived/']);
171
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'foo.md', 'user-me', 'notes', 'archived/foo.md', expect.anything());
172
+ });
173
+ it('non-recursive: literal dst path is used verbatim (no trailing slash)', async () => {
174
+ const transferBetweenScopes = vi.fn(async () => { });
175
+ const docs = makeDocs({ transferBetweenScopes });
176
+ const sh3 = makeSh3(personalScope);
177
+ const { ctx } = makeCtx(docs, sh3);
178
+ await xferVerb.run(ctx, ['@me:notes/foo.md', '@me:notes/archived/renamed.md']);
179
+ expect(transferBetweenScopes).toHaveBeenCalledWith('user-me', 'notes', 'foo.md', 'user-me', 'notes', 'archived/renamed.md', expect.anything());
180
+ });
107
181
  });
@@ -14,6 +14,7 @@
14
14
  * defensive pattern as `platform/index.ts`. Vite code-splits it
15
15
  * into a Tauri-only chunk that never loads in web builds.
16
16
  */
17
+ import { getEnvServerUrl } from '../env/serverUrl';
17
18
  import { getAuthToken } from './authToken';
18
19
  let tauriFetch = null;
19
20
  let tauriProbed = false;
@@ -43,8 +44,25 @@ async function getTauriFetch() {
43
44
  }
44
45
  return tauriFetch;
45
46
  }
47
+ // Resolve relative paths against the configured server URL so callers can
48
+ // use bare `/api/...` paths and have them hit the configured sh3-server
49
+ // regardless of the webview's origin. Mirrors ctx.fetch's resolveUrl —
50
+ // absolute URLs pass through, relatives get prefixed when a serverUrl is
51
+ // set. When no serverUrl is configured (e.g. early bootstrap, web builds
52
+ // hosted same-origin), the path is left untouched so `fetch` falls back
53
+ // to `window.location.origin` as before.
54
+ function resolveApiUrl(url) {
55
+ if (url.startsWith('http://') || url.startsWith('https://'))
56
+ return url;
57
+ const base = getEnvServerUrl();
58
+ if (!base)
59
+ return url;
60
+ const sep = url.startsWith('/') ? '' : '/';
61
+ return `${base}${sep}${url}`;
62
+ }
46
63
  export function apiFetch(url, init) {
47
64
  var _a;
65
+ const resolved = resolveApiUrl(url);
48
66
  // Inject Authorization: Bearer <session-token> if a session is active
49
67
  // and the caller didn't already supply one. Cookies don't survive the
50
68
  // cross-origin hop (SameSite=Lax + plugin-http has no cookie store),
@@ -62,11 +80,11 @@ export function apiFetch(url, init) {
62
80
  // call-timing assertions in tests still hold and web builds skip the
63
81
  // dynamic-import probe entirely.
64
82
  if (!inTauriRuntime()) {
65
- return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
83
+ return fetch(resolved, Object.assign({ credentials: 'include' }, finalInit));
66
84
  }
67
85
  return getTauriFetch().then((tf) => {
68
86
  if (tf)
69
- return tf(url, finalInit);
70
- return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
87
+ return tf(resolved, finalInit);
88
+ return fetch(resolved, Object.assign({ credentials: 'include' }, finalInit));
71
89
  });
72
90
  }
@@ -34,4 +34,67 @@ describe('apiFetch', () => {
34
34
  const [, init] = calls[0];
35
35
  expect(init.credentials).toBe('omit');
36
36
  });
37
+ it('resolves relative paths against the configured serverUrl', async () => {
38
+ const calls = [];
39
+ globalThis.fetch = vi.fn(async (input) => {
40
+ calls.push(String(input));
41
+ return new Response('ok');
42
+ });
43
+ const { __setEnvServerUrl } = await import('../env/serverUrl');
44
+ __setEnvServerUrl('https://remote.example.com');
45
+ try {
46
+ const { apiFetch } = await import('./apiFetch');
47
+ await apiFetch('/api/admin/users');
48
+ expect(calls[0]).toBe('https://remote.example.com/api/admin/users');
49
+ }
50
+ finally {
51
+ __setEnvServerUrl('');
52
+ }
53
+ });
54
+ it('prepends a slash to bare relative paths when serverUrl is set', async () => {
55
+ const calls = [];
56
+ globalThis.fetch = vi.fn(async (input) => {
57
+ calls.push(String(input));
58
+ return new Response('ok');
59
+ });
60
+ const { __setEnvServerUrl } = await import('../env/serverUrl');
61
+ __setEnvServerUrl('https://remote.example.com');
62
+ try {
63
+ const { apiFetch } = await import('./apiFetch');
64
+ await apiFetch('api/admin/users');
65
+ expect(calls[0]).toBe('https://remote.example.com/api/admin/users');
66
+ }
67
+ finally {
68
+ __setEnvServerUrl('');
69
+ }
70
+ });
71
+ it('passes absolute URLs through unchanged regardless of serverUrl', async () => {
72
+ const calls = [];
73
+ globalThis.fetch = vi.fn(async (input) => {
74
+ calls.push(String(input));
75
+ return new Response('ok');
76
+ });
77
+ const { __setEnvServerUrl } = await import('../env/serverUrl');
78
+ __setEnvServerUrl('https://remote.example.com');
79
+ try {
80
+ const { apiFetch } = await import('./apiFetch');
81
+ await apiFetch('https://other.example.com/api/bar');
82
+ expect(calls[0]).toBe('https://other.example.com/api/bar');
83
+ }
84
+ finally {
85
+ __setEnvServerUrl('');
86
+ }
87
+ });
88
+ it('leaves relative paths alone when no serverUrl is configured', async () => {
89
+ const calls = [];
90
+ globalThis.fetch = vi.fn(async (input) => {
91
+ calls.push(String(input));
92
+ return new Response('ok');
93
+ });
94
+ const { __setEnvServerUrl } = await import('../env/serverUrl');
95
+ __setEnvServerUrl('');
96
+ const { apiFetch } = await import('./apiFetch');
97
+ await apiFetch('/api/admin/users');
98
+ expect(calls[0]).toBe('/api/admin/users');
99
+ });
37
100
  });
@@ -209,14 +209,14 @@ export interface VerbContext {
209
209
  signal?: AbortSignal;
210
210
  }
211
211
  /**
212
- * Portable JSON Schema subset accepted by sh3-core for `Verb.schema.input`.
213
- * Documented as the intersection of what Anthropic, OpenAI, and Gemini
214
- * tool-call APIs accept natively. sh3-core does NOT validate that the
215
- * actual schema stays within this subset — authors who reach for `oneOf`,
216
- * `$ref`, or other Draft 2020-12 features do so at their own portability
217
- * risk. The type is intentionally `unknown`-permissive on `properties`
218
- * and `items` so authors can express object/array shapes without
219
- * fighting the type system.
212
+ * Portable JSON Schema subset accepted by sh3-core for `Verb.schema.input`
213
+ * and `Verb.schema.output`. Documented as the intersection of what
214
+ * Anthropic, OpenAI, and Gemini tool-call APIs accept natively. sh3-core
215
+ * does NOT validate that the actual schema stays within this subset —
216
+ * authors who reach for `oneOf`, `$ref`, or other Draft 2020-12 features
217
+ * do so at their own portability risk. The type is intentionally
218
+ * `unknown`-permissive on `properties` and `items` so authors can express
219
+ * object/array shapes without fighting the type system.
220
220
  */
221
221
  export interface PortableJSONSchema {
222
222
  type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
@@ -229,14 +229,39 @@ export interface PortableJSONSchema {
229
229
  required?: string[];
230
230
  /** Item schema for `type: 'array'`. */
231
231
  items?: PortableJSONSchema;
232
+ /**
233
+ * JSON Schema `format` hint. sh3-core blesses one value:
234
+ * - 'sh3-document' — runtime payload is an SH3 document handle
235
+ * ({ shardId: string; path: string }). Combine with type: 'object'.
236
+ * Other `format` values pass through untouched (portability with
237
+ * JSON Schema validators and tool-call APIs is preserved).
238
+ */
239
+ format?: string;
232
240
  }
233
241
  /**
234
- * Optional schema attached to a verb. Today only `input` is consumed
235
- * (by sh3-ai's tool-call dispatcher); `output` is deferred until
236
- * sh3-editor's graph view materializes a real consumer.
242
+ * Optional schema attached to a verb. `input` is consumed by sh3-ai's
243
+ * tool-call dispatcher and by sh3-pipeline's verb-adapter. `output`
244
+ * describes the shape of the verb's return value (the `result` field
245
+ * of `Sh3Api.runVerb`'s Promise resolution) and is consumed by
246
+ * sh3-pipeline to derive output ports.
237
247
  */
238
248
  export interface VerbSchema {
239
249
  input: PortableJSONSchema;
250
+ /**
251
+ * Shape of the verb's return value. When omitted, callers treat the
252
+ * verb as side-effect-only and read the scrollback instead.
253
+ *
254
+ * - {type:'object', properties:{…}} → result is an object whose
255
+ * own properties match the schema. Each top-level property is
256
+ * one logical output (one port for sh3-pipeline).
257
+ * - any other type → result IS that value.
258
+ *
259
+ * Document outputs are declared as
260
+ * { type: 'object', format: 'sh3-document',
261
+ * properties: { shardId: {type:'string'}, path: {type:'string'} } }
262
+ * or inlined inside a parent object's properties.
263
+ */
264
+ output?: PortableJSONSchema;
240
265
  }
241
266
  export interface Verb {
242
267
  name: string;
@@ -271,7 +296,19 @@ export interface Verb {
271
296
  * whether to read it or fall back to `args[]`.
272
297
  */
273
298
  schema?: VerbSchema;
274
- run(ctx: VerbContext, args: string[]): Promise<void>;
299
+ /**
300
+ * Returns the verb's structured result. The return value MUST match
301
+ * the shape declared by `schema.output` when both are present:
302
+ * - schema.output {type:'object', properties:{…}} → return object with those keys
303
+ * - schema.output primitive → return that primitive
304
+ * - schema.output undefined → return value is ignored
305
+ *
306
+ * Verbs that don't declare schema.output may return undefined; the
307
+ * Sh3Api.runVerb resolution surfaces it as `result: undefined`.
308
+ * Existing verbs that returned Promise<void> remain assignable —
309
+ * `void` is a subtype of `unknown` for return-type widening.
310
+ */
311
+ run(ctx: VerbContext, args: string[]): Promise<unknown>;
275
312
  }
276
313
  export type Resolution = {
277
314
  kind: 'local';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expectTypeOf } from 'vitest';
2
+ describe('VerbSchema with output', () => {
3
+ it('accepts output as PortableJSONSchema', () => {
4
+ const schema = {
5
+ input: { type: 'object', properties: { topic: { type: 'string' } } },
6
+ output: { type: 'object', properties: { answer: { type: 'string' } } },
7
+ };
8
+ expectTypeOf(schema.output).toEqualTypeOf();
9
+ });
10
+ it('accepts format on PortableJSONSchema', () => {
11
+ const docSchema = {
12
+ type: 'object',
13
+ format: 'sh3-document',
14
+ properties: {
15
+ shardId: { type: 'string' },
16
+ path: { type: 'string' },
17
+ },
18
+ };
19
+ expectTypeOf(docSchema.format).toEqualTypeOf();
20
+ });
21
+ });
22
+ describe('Verb.run return type', () => {
23
+ it('accepts a verb returning Promise<unknown>', () => {
24
+ const v = {
25
+ name: 'demo',
26
+ summary: 's',
27
+ async run() {
28
+ return { answer: 'ok' };
29
+ },
30
+ };
31
+ expectTypeOf(v.run).returns.toEqualTypeOf();
32
+ });
33
+ it('accepts a legacy verb returning Promise<void>', () => {
34
+ const v = {
35
+ name: 'legacy',
36
+ summary: 's',
37
+ async run() {
38
+ // returns nothing
39
+ },
40
+ };
41
+ expectTypeOf(v.run).returns.toEqualTypeOf();
42
+ });
43
+ });
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.23.2";
2
+ export declare const VERSION = "0.25.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.23.2';
2
+ export const VERSION = '0.25.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.23.2",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"